Skip to main content
版本:v2.0.0

EventMesh control plane

Audience: operators and contributors. What the runtime boot path wires from configuration, and how every state store fails when its backend goes down.


This page documents the control-plane components that the runtime boot path wires from configuration. For each component, we list the config key, the default value, the failure behavior when the key is missing or malformed, and the ingress route(s) that the component gates.

1. DeliveryTopology (#5293, #5309)

Selects how the runtime polls / dispatches events. Two modes:

ModeDefault?What it doesWhat it requires
LOCAL_STICKY_PULLYesEvery runtime instance polls every partition of every subscribed topic. No Meta store dependency. Backward compatible with pre-#5301 deployments.None
PARTITION_OWNED_PULLNoEach instance acquires ownership of a strict subset of partitions via Meta CAS + fencing. No duplicate consumption across instances.Meta store (Nacos today). Code is intact (see PartitionOwnership); the wiring follow-up is tracked in #5309.

1.1 Configuration

  • Key: eventmesh.delivery.topology
  • Default: LOCAL_STICKY_PULL (the value is read in EventMeshApplication via DeliveryTopology.fromConfig(System.getProperty(...))).
  • Set via: JVM -D flag (-Deventmesh.delivery.topology=PARTITION_OWNED_PULL) or the EVENTMESH_DELIVERY_TOPOLOGY environment variable (the runtime forwards env-var values into system properties at boot).
  • Documented in eventmesh.properties: the property is listed with a commented default and an inline explanation of both modes; operators can uncomment + edit without leaving the config file.

1.2 Failure behavior

  • Missing or blank -> resolved to LOCAL_STICKY_PULL (backward compatible). The startup log emits delivery topology=LOCAL_STICKY_PULL.
  • Unknown value (e.g. PARTITION_OWNER typo) -> IllegalArgumentException raised by DeliveryTopology.fromConfig. The runtime fails fast at boot; the operator sees a clear error in bin/start.sh output. A typo must not silently degrade to single-instance mode (see DeliveryTopologyTest).
  • PARTITION_OWNED_PULL without a Meta store -> enableCluster is required. The boot path throws if metaStore is null when the topology is PARTITION_OWNED_PULL.

1.3 Coverage per ingress route

The delivery topology is referenced from a single code path (UniRuntime.poll -> ownedPartitions(topic)), which is the consumer of UniIngressService.deliver. Therefore every ingress path is covered by construction:

Ingress routePath through topology
HTTP POST /events/publishUniHttpServer -> UniIngressService.publish -> Frame -> Producer.send -> poll loop -> topology-selected partition set
A2A POST /a2a/tasks/sendA2AGatewayHttpHandler -> A2AGatewayService -> UniIngressService -> same as above
Legacy TCP (UniTcpServer)main path is being phased out (see docs/feature/protocols.md #5341); when active, the TCP server feeds the same UniIngressService
WebSocket pushpassive receiver; not gated by topology (the topology decides which partitions to poll, not the push path)
SSEpassive receiver; not gated by topology

Coverage is verified by git grep of DeliveryTopology in the runtime source tree (only UniRuntime and DeliveryTopology itself should match in eventmesh-runtime/src/main/).

2. SecurityGate (#5304)

SecurityGate is the per-request control-plane gate that lives in front of UniIngressService and A2AGatewayService. It is composed of:

  • FilterChain (auth / acl): TokenAuthFilter, SignatureVerifierFilter, AclFilter.
  • QuotaManager (per-tenant): TenantQuotaManager.
  • AuditSink: pluggable (default is LoggingAuditSink).

2.1 Configuration

  • Key: eventmesh.security.gate.* (see eventmesh.properties section eventmesh.security.gate.* and the per-filter keys).
  • Default: a no-op gate that allows every request. This is the backward-compatible default and is documented as such in docs/quickstart/configuration.md.
  • Fail-closed mode: when eventmesh.security.gate.failClosed=true, any filter chain exception (auth failed, signature invalid, ACL denied) results in a 401/403 response. The default is fail-open (allowed) so a misconfigured gate does not silently break a deployment.

2.2 Failure behavior

  • Missing or blank -> no-op gate; every request is allowed. Operators who need a real gate must set the per-filter keys.
  • Malformed ACL entry -> logged at WARN, the request is denied (fail safe in the security context), the deny is counted in the gate's metrics counter.
  • Audit sink failure -> logged at ERROR, the request is still allowed (audit is best-effort; a failed audit must not take down the data path).

2.3 Coverage per ingress route

Ingress routeGated by SecurityGate?Where the gate is invoked
HTTP POST /events/publishYesUniHttpServer -> RequestContext -> SecurityGate.check(ctx, frame) -> UniIngressService.publish
HTTP POST /events/subscribeYessame chain
A2A POST /a2a/tasks/sendYesA2AGatewayHttpHandler -> gate -> A2AGatewayService -> UniIngressService
Legacy TCPYes (gated by TCP-level auth, separate from SecurityGate)UniTcpServer -> TcpIngressBridge (the TCP path has its own connection-level auth, see docs/feature/protocols.md)
Admin HTTPYesUniAdminServer -> UniAdminService (separate gate, configured under eventmesh.admin.security.*)
WebSocket pushn/a (passive)push does not invoke the gate; the gate ran at subscribe time
SSEn/a (passive)same as WS
Connector RuntimeYesConnectorScheduler -> gate (configured separately)

3. Configuration matrix

PropertyDefaultComponentEffect of the default
eventmesh.delivery.topologyLOCAL_STICKY_PULLDeliveryTopologySingle-instance / no Meta dependency
eventmesh.security.gate.failClosedfalseSecurityGateBackward-compatible allow-all
eventmesh.storage.type(commented out)Storage pluginMust be set to a supported backend
eventmesh.connector.offset.storememoryConnectorOffsets not persisted (dev-only)

4. References

  • Parent: #5296 (Architecture Review, Q2 / 2026-09-07)
  • Tracking: #5338
  • DeliveryTopology decision: #5293 (closed via PR #5308, implementing LOCAL_STICKY_PULL)
  • DeliveryTopology wiring follow-up: #5309 (PARTITION_OWNED_PULL wiring)
  • SecurityGate: #5304
  • Architecture: docs/architecture/overview.md (section 4, Security gate)
  • Configuration: docs/quickstart/configuration.md

State store failure matrix

Absorbed from the per-backend failure analysis (issue #5339). What happens when each store's backend goes down, and how the restart / multi-instance / fencing scenarios are covered by StateStoreDurabilityTest.

1. Store-to-backend map

StoreBackendTierProduction implTest impl
OffsetStoreRocksDB (local)L1RocksDBOffsetStoreInMemoryOffsetStore (degraded mode only)
SubscriptionStoreMeta + local cacheL2ClusterSubscriptionStore (watches /em/subs/)state.fault.InMemorySubscriptionStore
SessionStoreMeta + local cacheL2SessionRegistry (agents, bindings, sessions)direct SessionRegistry over InMemoryMetaStore
DeliveryStateStoreRocksDB (local, sub-second flush)L1RocksDBDeliveryStateStoreInMemoryDeliveryStateStore
DeadLetterStoreMeta CASL3MetaBackedDeadLetterStore (/em/dlq/)direct wrapper over InMemoryMetaStore
TaskStoreMeta CASL3MetaBackedTaskStore (/em/tasks/)direct wrapper over InMemoryMetaStore

2. Per-backend failure behavior

2.1 RocksDB backend (OffsetStore, DeliveryStateStore)

ScenarioBehaviorOperator-visible signalRecovery
Local disk fullwriteOffset / put returns false or throws IllegalStateExceptionRocksDBOffsetStore.offsetWriteFailures counter; ReliableDispatcher.pendingCount does not dropOperator frees disk; next write succeeds. The dispatcher keeps the delivery in flight (issue #5290).
Process kill -9 (no graceful close)The RocksDB file is the durability surface; the next process open at the same path sees the last flushed staten/a (no log emitted on the way down)The runtime calls UniRuntime.alignPullOffsetsToAck + ReliableDispatcher.recover() on boot. StateStoreDurabilityTest#DeliveryStateStoreKillMinusNine exercises this path.
Process restart (graceful)flush() is called by the shutdown hook, then close(). Re-open at the same path is a no-op for the persisted entriesn/aSame as kill -9.
Corrupted RocksDB fileRocksDB.open throws RocksDBException -> IllegalStateException at the constructor. The runtime fails fast at boot.IllegalStateException("failed to open RocksDB offset store at <path>") in the boot log.Operator restores from backup or wipes the data directory (data loss is unavoidable in this case; the runtime fails fast rather than silently losing state).

2.2 Meta backend (SubscriptionStore, SessionStore, DeadLetterStore, TaskStore)

All four Meta-backed stores use the same primitives:

  • MetaStore.putIfAbsent (CAS) for first-write-wins (DLQ, TaskStore.createTask).
  • MetaStore.tryAcquire(expectedOldValue, newValue) (CAS) for status updates (TaskStore.updateStatus).
  • MetaStore.put (last-write-wins) for plain updates (SubscriptionStore.put).
  • MetaStore.get / getWithPrefix for reads.
ScenarioBehaviorOperator-visible signalRecovery
Meta unreachable (network partition)put / putIfAbsent / tryAcquire throw RuntimeException (the production NacosMetaStore wraps the Nacos exception)The exception is caught at the call site; DeliveryDispatcher keeps the delivery in flight; A2AGatewayService surfaces the error to the A2A caller; ReliableDispatcher keeps the in-flight delivery in the ledgerOnce Meta heals, the next tick succeeds. Idempotent on the retry: DeadLetterStore.recordDeadLetter is idempotent on already-present keys; TaskStore.createTask returns null for a duplicate taskId (the gateway generates a fresh id and retries).
Meta cluster split-brain (two instances write the same key)First-write-wins (putIfAbsent) is correct; tryAcquire rejects the stale writer (returns false); plain put is last-write-winsThe losing writer observes false from the CAS call and re-readsOperator doesn't need to act; the contract is self-healing. StateStoreDurabilityTest#TaskStoreMetaFailure exercises the partitioned-write case.
Meta returns stale data (clock skew)MetaStore.tryAcquire(expectedOldValue=...) rejects writes whose expectedOldValue does not match the current Meta value; the caller re-readsThe call returns falseCaller re-reads via meta.get(key) and retries with the fresh value. The Meta-backed stores do not carry their own wall-clock dependency; staleness surfaces as a CAS mismatch, not as a time-based inconsistency.
Meta restarts (process restart of the Meta server)All reads return the snapshot from the most recent successful write; writes resume once the new Meta is reachableThe production NacosMetaStore reconnects via the Nacos client retry loop; the runtime logs the reconnection at INFONo operator action needed; the in-process caches (SessionRegistry.agentCache, ClusterSubscriptionStore.cache) are rebuilt from the next watch event. StateStoreDurabilityTest#DeadLetterStoreRestart exercises the wrapper-restart-over-shared-Meta case.
Local in-process cache diverges from MetaThe next read through MetaStore.get / getWithPrefix returns the Meta view; the in-process cache is refreshed on the next watch eventn/a (no log)Convergence is automatic; the local cache is best-effort.

3. Restart / multi-instance / fencing coverage

Each row maps an issue #5339 acceptance scenario to the test that exercises it.

#ScenarioStoreTest
1Restart: persist offset 100, kill runtime, restart, read offset = 100OffsetStore (RocksDB)StateStoreDurabilityTest#OffsetStoreRestart.offsetSurvivesProcessRestart
2Multi-instance: two ClusterSubscriptionStore instances sharing the same Meta; concurrent register / unregister convergesSubscriptionStoreStateStoreDurabilityTest#SubscriptionStoreMultiInstance.twoInstancesConvergeOnSameView (100-iteration storm, 8 threads) + removeIsObservedByAllInstances
3Fencing: stale partition owner (instance A) writes after instance B takes over; the write is rejectedSessionStoreStateStoreDurabilityTest#SessionStoreFencing.staleHeartbeatAfterTakeoverIsRejected
4Restart: DLQ ledger survives a wrapper restart (Meta is the durability surface)DeadLetterStore (Meta)StateStoreDurabilityTest#DeadLetterStoreRestart.ledgerSurvivesStoreWrapperRestart + writeSucceedsAfterMetaHeals
5Cross-store failure: Meta goes down mid-submit; the TaskStore surfaces the failure rather than silently dropping the taskTaskStore (Meta)StateStoreDurabilityTest#TaskStoreMetaFailure.createTaskFailsWhenMetaIsPartitioned + updateStatusFailsWhenMetaIsPartitioned
6Kill -9: in-flight ledger survives abrupt closeDeliveryStateStore (RocksDB)StateStoreDurabilityTest#DeliveryStateStoreKillMinusNine.inFlightLedgerSurvivesAbruptClose

4. Acceptance check (for #5339)

  • For each of the 6 stores above, a JUnit test exists in eventmesh-runtime (StateStoreDurabilityTest).
  • The test for each scenario above is referenced in the table in section 3.
  • No production-code store is backed by an InMemory* implementation; the in-memory implementations live in state/fault/ (test-only).
  • This document exists (docs/feature/control-plane.md).

5. References

  • Parent: #5296 (Architecture Review, Q3 / 2026-09-07)
  • Tracking: #5339
  • Tests: eventmesh-runtime/src/test/java/org/apache/eventmesh/runtime/state/StateStoreDurabilityTest.java
  • Related: #5289 (monotonic offset), #5290 (write failure semantics), #5291 (idempotency), #5292 (DLQ durability), #5301 (state control plane)
  • Sub-PRs:
    • #5301 Sub-PR A (interfaces + SPI)
    • #5301 Sub-PR B (RocksDB-backed delivery + offset stores)
    • #5301 Sub-PR C (Meta-backed DLQ + TaskStore)
    • #5301 Sub-PR D (A2A Gateway on Runtime)