blob: 35866bde49dcd202d5f94d0fbd59c817ec6fefff [file] [view]
# EventMesh Architecture Simplification Redesign
> **Status note:** this is the historical design document for the
> architecture rewrite (a design record, not a living guide). The current
> implementation status and maturity of each capability are governed by the
> [capability status table](../../README.md#capability-status) in the main
> README. For the user-facing views see
> [Architecture overview](overview.md) and the
> [feature guides](../feature/).
> Branch: `refactor/unified-runtime-pipeline` — based on the direction
> defined in this document.
>
> **Core change:** *drop the MQ's Producer Group / Consumer Group semantics
> entirely — the MQ is used purely as a storage layer, and EventMesh
> maintains its own subscription and dispatch semantics. Internally
> everything is `EventMeshFrame` (CloudEvent is demoted to one of the
> external ingress formats); externally the multi-protocol surface
> (CloudEvents / MeshMessage / A2A) converts directly to Frame via the
> `FrameAdaptor` SPI, never through each other. The SDK is simplified to
> HTTP-only.*
>
> **v2.0 architecture deepening (2026-08-13, absorbed from
> `eventmesh-architecture-refinement.md`):**
>
> - `EventMeshFrame` end-to-end internally (storage SPI + dispatch pipeline
> + carriers all Frame-typed).
> - External protocol conversion moved into the `FrameAdaptor` SPI
> (CloudEvents / MeshMessage / A2A each an independent plugin, zero
> CE-intermediary conversions).
> - Offset path-② (purely local, no meta reporting, no group.id; takeover
> via MQ replay).
> - Full-stickiness load balancing (session-assignment-layer `recommend`,
> self-reported instance load, no forwarding).
---
## Executive summary
The pre-rewrite architecture inherited Kafka/RocketMQ's Producer Group /
Consumer Group concepts, which caused:
- **SDK complexity** — clients had to understand Group, Topic, Tag and
other MQ semantics; a steep learning curve.
- **Semantic confusion** — EventMesh's own subscription model
(load-balanced / broadcast / multicast) was entangled with the MQ Group
concept.
- **Multi-protocol burden** — TCP + HTTP + gRPC SDKs maintained in
parallel; protocol adaptation code bloated.
**Redesign direction: EventMesh becomes a pure "CloudEvents over MQ" event
bus. The MQ is EventMesh's storage backend; EventMesh manages its own
subscription and dispatch logic; clients only need HTTP + CloudEvents.**
---
## 1. Core design principles
### 1.1 The iron rules
| Rule | Meaning |
|------|---------|
| **MQ without semantics** | Kafka/RocketMQ are used only as "a durable FIFO queue"; no Producer Group / Consumer Group / Tag concept is ever exposed to clients |
| **EventMesh-owned subscriptions** | All dispatch logic (who receives which message, under which policy) is maintained by EventMesh itself, never delegated to the MQ |
| **EventMesh-owned offsets** | EventMesh manages the consumption position of every subscription (topic#clientId#partition → offset), modeled on the RocketMQ client OffsetStore, persisted in RocksDB — independent of MQ Consumer Group offsets |
| **Minimal SDK** | One SDK (HTTP), two objects (CloudEvent + Subscription), three APIs (publish / subscribe / unsubscribe) |
| **EventMeshFrame internally** | Everything internal is `EventMeshFrame` (fixed header + KV attributes + raw data); CloudEvent is demoted to one external ingress format; external protocols (CloudEvents/MeshMessage/A2A) each convert directly to Frame via the `FrameAdaptor` SPI, never through each other |
| **Independently deployed connectors** | The Connector Runtime is a separate process talking to the EventMesh Runtime over HTTP + CloudEvents — no shared internals, independent lifecycles and OffsetStores |
> **Implementation status snapshot (v2.0 / 2026-08-13 audit):** of the iron
> rules, "EventMeshFrame internally / independent connectors / minimal
> HTTP-only SDK" have landed; "EventMesh-owned subscriptions / offsets" hold
> for single-instance (offset path-② purely local + MQ-replay takeover,
> meta reporting off by default). Multi-instance coordination moved to
> **full stickiness** (session-assignment-layer recommend, forwarding layer
> disabled). External protocols go through the `FrameAdaptor` SPI
> (CloudEvents as an independent plugin + MeshMessage + A2A each converting
> directly to Frame, zero CE-intermediary). See §19.
### 1.2 The essential difference from the previous design
```
previous design redesigned
EventMesh SDK:
├─ TCP SDK (Package protocol) → deleted entirely
├─ HTTP SDK (EventMeshMessage) → simplified to CloudEvents-only
└─ gRPC SDK (proto CloudEvent) → deleted entirely
MQ role:
├─ Group semantics exposed to clients ───→ pure storage, no semantics
├─ MQ-side load balancing ───→ EventMesh SubscriptionManager
└─ Tag filtering (subExpression) ───→ EventMesh MULTICAST CloudEvent filters
```
---
## 2. Target architecture overview
```
┌─────────────────────────────────────────────────────────────────┐
│ EventMesh Client │
│ (HTTP SDK, CloudEvents-only) │
│ cloudEventsClient.publish(CloudEvent) │
│ cloudEventsClient.subscribe(topic, handler) │
│ cloudEventsClient.unsubscribe(topic) │
└────────────────────────────┬────────────────────────────────────┘
│ HTTP POST / GET (application/cloudevents+json)
▼
┌─────────────────────────────────────────────────────────────────┐
│ EventMesh Runtime │
│ (single process, unified) │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ HTTP endpoint (the only transport layer) │ │
│ │ POST /events/publish → Ingress pipeline │ │
│ │ POST /events/subscribe → SubscriptionManager │ │
│ │ POST /events/unsubscribe │ │
│ │ GET /events/poll → PushService delivery │ │
│ └──────────────────────────────┬─────────────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Ingress pipeline │ │
│ │ AuthFilter → RateLimitFilter → AclFilter │ │
│ │ → ProtocolFilter → TransformerEngine → RouterEngine │ │
│ └──────────────────────────────┬─────────────────────────────┘ │
│ ▼ EventMeshFrame │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Storage plugin (the MQ as a storage layer) │ │
│ │ · single Producer, EventMesh-managed │ │
│ │ · writes by topic partition │ │
│ │ · no Producer Group / clientId / producerGroup config │ │
│ │ · single Consumer — pulls everything, dispatch per │ │
│ │ EventMesh subscription rules, no Consumer Group │ │
│ └──────────────────────────────┬─────────────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ SubscriptionManager (the core) │ │
│ │ manages every client subscription; dispatches by policy: │ │
│ │ · LOAD_BALANCE (round-robin / least-connections) │ │
│ │ · BROADCAST (every subscriber) │ │
│ │ · MULTICAST (subject/type/header match) │ │
│ │ · own OffsetStore (RocksDB) │ │
│ └──────────────────────────────┬─────────────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ PushService (long-poll / SSE / WebSocket transports) │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│ Kafka / RocketMQ (pure storage)
▼
┌──────────────────────┐
│ Kafka / RocketMQ │
│ (a distributed WAL) │
│ · no Producer Group │
│ · no Consumer Group │
│ · no Tag filtering │
│ · EventMesh is the │
│ only producer + │
│ consumer │
└──────────────────────┘
```
---
## 3. Storage plugin redesign: the MQ as stateless storage
**The problem being removed.** The old storage plugin exposed MQ semantics:
```java
// eventmesh-storage-rocketmq (old)
MeshMQProducer.java:
producerGroup: String // RocketMQ ProducerGroup — clients had to understand it
createTransactionProducer() // transactional messages EventMesh had to inherit
MeshMQConsumer.java:
consumerGroup: String // RocketMQ ConsumerGroup — the core problem
subscribe(topic, subExpression) // subExpression = RocketMQ Tag filtering
push() / pull() // two consumption modes mixed
```
Configuring `consumerGroup` through the EventMesh SDK means the client is
still using RocketMQ's Consumer Group semantics — EventMesh was not really
providing its own subscription/dispatch model.
**The target interface** (landed as `MeshStoragePlugin`): only two core
concepts — `send(frame)` and offset-scoped `poll`, plus
`assignPartitions` / `commitOffset` — with no producerGroup, consumerGroup,
or tag parameters. *(Status note: the Kafka implementation initially kept
an internal `group.id` (gap G7); assignPartitions/commitOffset landed for
Kafka first and were partially stubbed for RocketMQ.)*
---
## 5. SDK minimization: the HTTP-family CloudEvents SDK
The old SDK family (TCP / HTTP / gRPC) had three object models and API
styles in parallel. The redesign collapsed them into **one HTTP SDK** with
`CloudEventsClient` exposing publish / subscribe / unsubscribe (plus the
later additions: request-reply, streaming, lite topics — see the
[client guide](../feature/client-java.md)).
---
## 6. Runtime ingress redesign: the unified IngressHandler
All entry points (HTTP publish, A2A, legacy bridge) converge on one
frame-typed ingress path — today's `UniIngressService` — so filters,
quota, and rate limiting have exactly one place to live.
---
## 7. Runtime egress redesign: PushService replaces the Consumer family
The old consumer model (TCP Consumer with session maps, HTTP PushConsumer
with LRU caches, gRPC push) was replaced by a single `PushService` with
pluggable transports (long-poll / SSE / WebSocket) fed by the
`SubscriptionManager` — all frame-typed, all sharing ACK / retry / quota.
---
## 8. Connector Runtime: fully independent
The Connector Runtime and EventMesh Runtime are **two completely
independent processes** communicating over HTTP + CloudEvents, sharing no
internal components:
```
┌─────────────────────────────┐ HTTP / CloudEvents ┌──────────────────────────────┐
│ Connector Runtime │ ←────────────────────→ │ EventMesh Runtime │
│ Source/Sink manager │ POST /events/publish │ ingress → storage (MQ) │
│ · connector lifecycle │ │ SubscriptionManager │
│ · external poll/write │ GET /events/poll │ · own offsets (RocksDB) │
│ · independent offsets │ ←───────────────────── │ · dispatch (LB/Bcast/Mcast)│
└─────────────────────────────┘ └──────────────────────────────┘
```
Source connectors `poll()` external systems into CloudEvents; sink
connectors `put(CloudEvent)` out; each maintains its own offsets.
---
## 9–11. Entry-point unification, deletions, phased plan
A single launcher (`EventMeshApplication`) starts traffic HTTP + admin (+
opt-in WebSocket). The deletion list removed the TCP/gRPC server paths,
`eventmesh-registry`, and dead modules (~59% of the tree). The phased plan
tracked the migration; the evidence trail lives in
[architecture review evidence](review/evidence.md).
---
## 12. Key design decision discussions
- **MQ without semantics vs keeping MQ capabilities** — the user's explicit
requirement: the MQ only stores. Partition load-balancing semantics →
replaced by the EventMesh SubscriptionManager; Tag filtering → replaced
by MULTICAST-mode CloudEvent filters; the MQ's durable WAL and offset
storage are kept (EventMesh replay builds on them).
- **Long-polling vs WebSocket / SSE** — the first cut chose long-polling;
decision §15.6 superseded it with **WebSocket default + SSE one-way
streaming + long-polling fallback**, all three user-selectable.
---
## 15. User decision log (v1.2)
Foundational decisions, confirmed 2026-07-02. Later design and
implementation follow this section; changes must be appended as new
decisions noting what they supersede.
### 15.1 MQ semantic boundary: fully self-managed coordination
Strict adherence to the "MQ without semantics" rule — EventMesh implements
multi-instance consumption coordination **entirely itself**: a
self-written partition-assignment protocol + leases + centralized offset
storage + cluster-wide subscription sync. No compromise that reuses MQ
Consumer Group rebalancing internally. Highest development cost; strongest
autonomy.
*(v2.0 note: multi-instance coordination was later simplified to the
full-stickiness model — see [Load balancing](../feature/load-balancing.md)
— with partition ownership retained behind `PARTITION_OWNED_PULL`.)*
### 15.2 Delivery semantics: at-least-once + client idempotency
Client-facing delivery is **at-least-once**, with clients deduplicating on
the CloudEvents `id`. Exactly-once is *not* implemented for the delivery
side. ACK + retry + DLQ + STICKY ordering + client dedup all hold.
Exactly-once remains confined to connector offset management (see §16).
### 15.3 SDK deletion scope: remove TCP + gRPC entirely, HTTP only
Keep a single HTTP-only CloudEvents SDK; **delete** the TCP and gRPC SDKs.
This is an **irreversible action** — all existing TCP/gRPC clients must
migrate to the HTTP SDK. Low-latency needs are covered by the optional
WebSocket/SSE transports. *(The shipped runtime keeps the legacy TCP path
as a compatibility bridge; see [Protocols & SDKs](../feature/protocols.md).)*
### 15.4 Landing baseline
The code baseline is the then-current workspace (which had already
absorbed meta / openconnect / A2A / CloudEvents / retry modules), not a
clean rewrite on Apache develop. Customizations (the masa submodules:
connector-wemq, trace-*, registry-*, security-acl) are plugin-shaped and
were slated for rewrite against the new SPIs.
### 15.5 Global control plane: the Meta registry (not the Admin Server)
The global control plane belongs to the **Meta registry** (instance
heartbeats, partition assignments, clientId routing, cluster subscription
view, offset replicas, dynamic rules); the Admin Server only manages
(Jobs, DLQ browse/replay, ops commands, metric aggregation) and never
performs strongly-consistent coordination. Degradation: Meta down →
instances self-assign; Admin down → ops impaired, Runtime fine. The dual
registry abstraction (`eventmesh-registry`) was dropped in favor of the
single Meta path.
### 15.6 Transport: the HTTP family, WebSocket default
- **WebSocket** (default): persistent bidirectional subscription streams,
high throughput, control commands on the same connection.
- **SSE**: one-way streaming output (LLM token streams, A2A streamed
replies); traverses firewalls well.
- **Long-polling**: the fallback when WS is blocked.
- **No gRPC**: WebSocket delivers the same low latency + throughput while
staying in the HTTP family (no proto files, no separate SDK).
### 15.7 request-reply added
A synchronous `request()` API over HTTP request-response (suspended
request, correlationId matching, Meta routing for cross-instance replies).
**Timeout = failure; late replies are dropped by default** (clear
semantics, no DLQ). request-reply is RPC semantics — no re-delivery, no
DLQ — deliberately isolated from the at-least-once pub/sub path.
### 15.8 Storage: S3Stream as an additional backend + Java 21
The implementation targets **Java 21** (virtual threads for suspended
push channels — near-zero cost per parked connection). Storage keeps
multiple backends with **S3Stream added** alongside Kafka/RocketMQ
(integration posture A: a minimal `S3StreamStoragePlugin`; EventMesh still
fully self-coordinates partitions; S3Stream is a data WAL only).
RocksDB's clarified position: the **local full copy + Meta write-offload +
degraded fallback** for offsets — not redundant with the Meta tier.
---
## 16. Delivery semantics boundary: at-least-once vs exactly-once
Two different layers, often confused:
```
Layer A: connector-internal progress (offset persistence)
Scope: Source pull offset / Sink write-confirm offset
Where: inside the Connector Runtime; no external clients involved
Semantics: exactly-once (local RocksDB + remote Admin double-write)
Purpose: a connector restart neither loses nor double-processes external data
Layer B: client-facing delivery (subscribers receiving messages)
Scope: EventMesh → SDK subscriber (post-publish delivery)
Where: SubscriptionManager → PushService → client poll/ACK
Semantics: at-least-once + client idempotency (decision 15.2)
Purpose: no message loss; duplicates possible, deduped by id at the client
```
Exactly-once is *feasible* for layer A (self-contained offset state) and
*prohibitively expensive* for layer B (transactional delivery + a global
dedup store) — hence the split.
---
## 17. request-reply (v1.3)
```
requester: POST /events/request
body: CloudEvent { ..., x-em-reply-to: "reply.<reqId>", x-em-correlation-id: reqId }
(the HTTP request suspends, blocking for a reply or timeout)
↓ EventMesh routes like a publish (ingress → storage.send)
responder: subscribed to the request topic, processes, then
POST /events/reply
body: CloudEvent { x-em-correlation-id: reqId, data: reply }
↓ EventMesh matches correlationId to the suspended request
requester: the suspended request returns the reply ← unblocked
timeout → the request fails (same semantics as the old TCP sync call)
```
Key points: a correlationId→AsyncContext matching table with timeout
sweep; cross-instance replies route via the Meta routing table; late
replies are dropped; never re-delivered and never dead-lettered (RPC side
effects must not repeat). *(Gap G10: self-addressed cross-instance reply
routing was later covered by the sticky-instance model.)*
---
## 18–21. Test design, streaming, and the SDK streaming API
The end-to-end test-case design (§18) mapped every guarantee above to a
named test; the current test inventory and per-PR evidence live in
[review evidence](review/evidence.md).
The streaming design (§20–21, integrated from the streaming-session,
sdk-streaming-call, and lite-streaming-call designs) defines the two
orthogonal streaming modes — **Mode 1** (client → agent streaming call;
the runtime mediates, channels multiplex with client affinity; sessionId
format `<agentId>:<uuid>` routes with zero lookups on the `:` prefix) and
**Mode 2** (session pub/sub with deterministic lite naming, no agent, no
matchmaking) — both carried by `EventMeshFrame` STREAM_REQ /
STREAM_CHUNK messages. The SDK surface is `client.streaming()`:
`openSession → call → forEach` and `subscribeSession` /
`openSessionPublisher`. See
[Streaming & push transports](../feature/streaming.md) for the current
user-facing guide.
---
## Appendices (historical)
Appendices A–F of the original document carried the configuration key
table, the full CloudEvents extension field set, deployment topology
examples, the develop-code migration mapping, the customization (masa)
migration inventory, and the v1.11 implementation-gap analysis. The
still-current content from those appendices lives in the feature and
reference pages of this documentation tree (configuration, protocols,
offset management, load balancing, frame protocol); the point-in-time
gap/migration tables are superseded by the
[architecture review evidence](review/evidence.md) and are intentionally
not reproduced.
*Document version: v2.0 (English rewrite of the Chinese original) ·
integrated 2026-08-13 · related: [offset management](../feature/offset-management.md),
[load balancing](../feature/load-balancing.md),
[frame protocol](../feature/frame-protocol.md).*