Skip to content

feat(cluster): PARTITION_OWNED_PULL delivery topology (issue #5309, closes) - #5317

Merged
qqeasonchen merged 10 commits into
apache:developfrom
qqeasonchen:feat/partition-owned-pull
Aug 31, 2026
Merged

feat(cluster): PARTITION_OWNED_PULL delivery topology (issue #5309, closes)#5317
qqeasonchen merged 10 commits into
apache:developfrom
qqeasonchen:feat/partition-owned-pull

Conversation

@qqeasonchen

@qqeasonchen qqeasonchen commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Overview

feat(cluster): PARTITION_OWNED_PULL delivery topology (issue #5309, closes).

One-shot full PR covering all 5 sub-steps of #5309.

Changes (5 commits)

  1. c50dcd9feat(cluster): add DeliveryTopology enum + fail-fast parser
    • New DeliveryTopology enum (LOCAL_STICKY_PULL / PARTITION_OWNED_PULL)
    • fromConfig(String) parser: null/empty → default; unknown value → IllegalArgumentException (fail-fast)
  2. f9d33a5feat(cluster): wire PARTITION_OWNED_PULL topology into UniRuntime
    • Split constructors: original 6-arg kept (default LOCAL_STICKY_PULL, "standalone"), new 9-arg added
    • start() invokes startPartitionOwnership() when topology == PARTITION_OWNED_PULL
    • startPartitionOwnership() constructs ClusterMembership + PartitionOwnership + InMemoryMetaStore
    • shutdown() calls stopPartitionOwnership() first to release Meta assignment records
  3. 3de32f6test(cluster): PARTITION_OWNED_PULL wiring E2E + parser tests
    • DeliveryTopologyTest (7 cases): null/empty/blank/exact name/trim/unknown value/lowercase
    • UniRuntimeTopologyWiringTest (3 cases): two instances of PARTITION_OWNED_PULL converge to a full-coverage disjoint split; LOCAL_STICKY_PULL never assigns partitions; null topology fails fast
  4. 1747c20docs(uni-arch): add DeliveryTopology dual-topology matrix to §13.2.10
    • Updates the §13.2 status banner (blocker cleared)
    • Adds §13.2.10 dual-topology matrix: 9-dimension comparison + selection semantics + startup flow + diff vs the original §13.2.8 design + verification coverage
  5. 936681cfix(docs): rename dual-topology section to §13.2.11

Dual-Topology Matrix Summary

Dimension LOCAL_STICKY_PULL (default) PARTITION_OWNED_PULL
Use case Single instance / low throughput Production HA / scale-out
Meta dependency None Required (Nacos / etcd / ZK)
Startup cost Zero ClusterMembership + PartitionOwnership + heartbeat
Code entry 6-arg constructor (unchanged) 9-arg constructor + withClusterMeta() injection

Verification

  • ./gradlew :eventmesh-runtime:checkstyleMain :checkstyleTest passes
  • :eventmesh-runtime:test --tests DeliveryTopologyTest 7/7 pass
  • :eventmesh-runtime:test --tests UniRuntimeTopologyWiringTest 3/3 pass
  • :eventmesh-runtime:test --tests ClusterDeliveryFaultTest 5/5 pass (no regression)
  • :eventmesh-runtime:test --tests UniRuntimeTest 1/1 pass (no regression)

Follow-ups

  • Testcontainers (Kafka + Nacos) E2E test: not included in this PR (no Docker available in the dev environment; covered by a follow-up)
  • EventMeshApplication integration with the eventmesh.delivery.topology config key: not included in this PR

Related

…5302 Sub-PR D1)

Brings the A2A Gateway back into the Runtime, this time with the unified
control-plane stores from apache#5301 Sub-PR A/C as the durable backend instead
of the in-memory TaskRegistry that PR apache#5260 introduced.

What lands
----------
* A2AGatewayService — task lifecycle controller. Replaces the in-memory
  TaskRegistry with TaskStore (Sub-PR A/C). A small runtime cache holds
  the two fields TaskStore does not model: parentTaskId (used to render
  child-task queries) and taskEpoch (the per-task counter the store
  requires for stale-write rejection on updateStatus). Both caches are
  rebuildable from the store on a fresh JVM; the cache keys are
  taskIds, so the cache size tracks active tasks only.
* A2AGatewayHttpHandler — Netty HTTP handler for the REST + SSE API
  (/a2a/tasks, /a2a/tasks/{id}, /a2a/tasks/{id}/stream, /a2a/health).
  SSE registers a StatusSubscriber so intermediate PENDING -> RUNNING ->
  COMPLETED transitions stream to the client.
* A2AGatewayServer — Netty HTTP bootstrap. Production wires
  EventMeshA2ATransport (Runtime-bridged) for the A2AMessageTransport
  dependency; tests pass an in-process transport. The weather-agent
  demo from PR apache#5260 is removed — the demo client (A2AGatewayDemo) and
  the Meta-ized AgentCard registry land in Sub-PR D2.
* AgentCardRegistry + InMemoryAgentCardRegistry — minimal discovery
  surface (isAgentRegistered / registerCard / getCard). A Meta-backed
  implementation backed by SessionStore (Sub-PR A) lands in D2.

Status mapping (PR apache#5260 -> Sub-PR A/C)
---------------------------------------
  SUBMITTED -> PENDING, WORKING -> RUNNING, CANCELLED -> CANCELED (one L)
The legacy wire vocabulary stays available via A2AGatewayService.TaskState
and the toLegacyState() helper. JSON response payloads still emit
SUBMITTED/WORKING/CANCELLED so existing A2A clients do not break.

Documentation
-------------
* docs/a2a-protocol/README.md carries an EXPERIMENTAL banner that lists
  the three pieces still missing for production (TaskExpirer, Meta-ized
  AgentCard, Testcontainers E2E) — all scheduled for Sub-PR D2.

Acceptance against issue apache#5302
------------------------------
* "A2A transport over the Runtime delivery path" — A2AGatewayService
  takes an A2AMessageTransport; production wires EventMeshA2ATransport
  which already bridges A2A onto UniIngressService (issue apache#5301 Sub-PR
  A's runtime ingress). The parallel InMemoryA2AMessageTransport is no
  longer required by the gateway path.
* "A2A tasks persist through TaskStore and recover across restarts" —
  createTask / updateStatus / getTask all go through the persistent
  store. On a fresh JVM, a gateway that had pending tasks sees them
  with their last persisted status; in-flight work that was awaiting
  a response will be picked up on the next status callback.

Tests (7, all passing)
----------------------
* A2AGatewayServiceTest — 6 tests: create+complete, cancel, cancel-on-
  unknown, submit-to-unregistered, parent-child index, task-not-found.
  Uses an in-process TaskStore (mirrors Sub-PR A's test stub) and an
  in-process A2AMessageTransport that handles A2A's + single-segment
  wildcards.
* A2AGatewaySmokeTest — Netty HTTP /a2a/health loopback check.

Not in D1 (Sub-PR D2)
---------------------
* TaskExpirer reaper (periodic TaskStore.expireStale sweep).
* Meta-backed AgentCardRegistry (SessionStore).
* Testcontainers fault-injection E2E.
…5313)

A2AGatewayHttpHandler.java:
- Remove unused imports (A2AProtocolConstants, A2ATopicFactory, TaskState)
- Expand try-catch blocks for limit/offset parsing (EmptyCatchBlock, NeedBraces)
- Expand handleList loop control flow (NeedBraces)
- Collapse duplicate blank line between package and imports (EmptyLineSeparator)

A2AGatewayServiceTest.java:
- Expand single-line if-blocks in InProcessTaskStore.listByAgent
- Expand if-block in InProcessTransport.matches
- Expand if-blocks in tearDown
- Move taskId declaration closer to its use (VariableDeclarationUsageDistance)

A2AGatewaySmokeTest.java:
- Rewrite StubTaskStore / NoopTransport with multi-line method bodies
  (LeftCurly, RightCurlyAlone, NeedBraces, EmptyLineSeparator)
- Add missing static imports (assertEquals, assertNotNull)

Build PR apache#5313 fix.
…owup)

A2AGatewayServiceTest.java:
- Fix ImportOrder: AgentCapabilities before AgentCard (alphabetical)
- Fix VariableDeclarationUsageDistance for f: inline taskId into
  submitTask call so the declaration is closer to first use

A2AGatewaySmokeTest.java:
- Fix ImportOrder: static Assertions imports before non-static
  TaskStore import

Build PR apache#5313 followup.
…ollowup)

The previous fixup broke the test logic by calling TaskResult.getTaskId()
which does not exist. Restore the original taskId declaration pattern
while keeping the f declaration within 3 lines of its first use:

- Declare taskId first
- Declare f immediately after
- Inline cancelTask result into the assertTrue (no intermediate boolean)
- Move f.get() call right after the cancel assertion so f's first use
  is within 3 lines of its declaration

Build PR apache#5313 followup.
…ache#5309)

Sub-step 2 of apache#5309: PARTITION_OWNED_PULL delivery topology.

Adds DeliveryTopology enum with two modes:/n- LOCAL_STICKY_PULL (default): every instance polls all partitions
- PARTITION_OWNED_PULL: CAS+fencing ownership per partition (scale-out)

fromConfig() parses eventmesh.delivery.topology property:/n- null/blank -> LOCAL_STICKY_PULL (backward compatible)
- unknown -> IllegalArgumentException (fail-fast)
…ache#5309 sub-step 3)

Boots PartitionOwnership state machine when topology=PARTITION_OWNED_PULL:/n- New 9-arg constructor with DeliveryTopology, instanceId, instanceAddress
- start() calls startPartitionOwnership() after offset alignment
- startPartitionOwnership(): ClusterMembership + PartitionOwnership + InMemoryMetaStore
- shutdown() calls stopPartitionOwnership() before pull-loop shutdown
- Backward-compatible 6-arg constructor defaults to LOCAL_STICKY_PULL
…ser tests (apache#5309 sub-step 4)

UniRuntimeTopologyWiringTest boots real UniRuntime instances (full start()/shutdown()
lifecycle, real scheduler) against a shared InMemoryMetaStore:/n- two PARTITION_OWNED_PULL instances converge on a disjoint, full-coverage split
- LOCAL_STICKY_PULL default (6-arg ctor) never assigns partitions (poll-all)
- null topology fails fast in the constructor

DeliveryTopologyTest covers the fromConfig parser: null/blank -> default, exact names,
whitespace trim, unknown/lowercase -> IllegalArgumentException.

Complements ClusterDeliveryFaultTest (deterministic refreshOnce fault injection without a
runtime). A full Testcontainers Kafka+Nacos E2E is tracked as a follow-up.
… (issue apache#5309 sub-step 5)

Updates the §13.2 status banner (blocker closed) and adds §13.2.10 documenting the
two legitimate topologies:
- LOCAL_STICKY_PULL: default, single-instance, no Meta dependency (backward compatible)
- PARTITION_OWNED_PULL: scale-out, requires Meta CAS + fencing (production HA)

Includes selection semantics (fromConfig), startup flow (UniRuntime.start partition
ownership), diff vs the §13.2.8 original design, and verification coverage
(unit + E2E wiring + fault injection; Testcontainers deferred as a follow-up).
@qqeasonchen
qqeasonchen merged commit e279261 into apache:develop Aug 31, 2026
7 checks passed
qqeasonchen added a commit to qqeasonchen/eventmesh that referenced this pull request Aug 31, 2026
…pache#5314)

Documents the 6-scenario CrossStoreFaultInjectionTest: why single-store contract
tests cannot cover cross-store invariants, the four harness primitives, the
per-scenario assertion table, why Testcontainers is not used, and the two
extension points (RocksDB-backed crash scenario, MetaBackedOffsetStore going
active).

Numbered 13.2.12 because §13.2.11 was taken by the dual-topology matrix in
PR apache#5317.
qqeasonchen added a commit to qqeasonchen/eventmesh that referenced this pull request Aug 31, 2026
…pache#5314)

Documents the 6-scenario CrossStoreFaultInjectionTest: why single-store contract
tests cannot cover cross-store invariants, the four harness primitives, the
per-scenario assertion table, why Testcontainers is not used, and the two
extension points (RocksDB-backed crash scenario, MetaBackedOffsetStore going
active).

Numbered 13.2.12 because §13.2.11 was taken by the dual-topology matrix in
PR apache#5317.
qqeasonchen added a commit that referenced this pull request Aug 31, 2026
…ntrol plane (issue #5314) (#5318)

* test(state): add cross-store fault-injection harness (issue #5314)

Four in-process primitives shared by the six #5314 scenarios:

  MetaPartitionSwitch      - MetaStore wrapper; open()/close() simulates a network
                             partition (mutating ops throw MetaPartitionException,
                             reads continue against the pre-partition snapshot).
  CrossStoreRaceProbe      - ordered log of cross-store operations (DELIVERY_PUT/
                             REMOVE, OFFSET_WRITE/READ, TASK_UPDATE) with a monotonic
                             seq so a test can assert happens-before relationships.
  JvmCrashHarness          - child JVM + sentinel-file-driven destroyForcibly()
                             (SIGKILL / TerminateProcess), then relaunch against the
                             same on-disk stores. Gated on ENABLE_JVM_CRASH_HARNESS.
  InMemorySubscriptionStore- ConcurrentHashMap-backed SubscriptionStore for the
                             split-brain scenario (two views of the world).

All four are test-only and run fully in-process: no Nacos, no Docker, no
Testcontainers. See §13.2.12 of the architecture doc (added in a follow-up commit).

* test(state): add cross-store fault-injection 6-scenario test (issue #5314)

CrossStoreFaultInjectionTest covers the fault modes that span two or more
stores (or the runtime + cluster-shared Meta). The individual store contract
tests (#5310/#5311/#5312/#5313) cannot observe these, because the invariant
lives at the seam:

  1. CrashMidAckReAck  - crash after offset-write, before MQ-ACK callback;
                        recovery retires without re-invoking the channel
                        (issue #5291 idempotency).
  2. MetaPartitionDuringDlq - Meta unreachable while dead-letter recording;
                        the store throws MetaPartitionException rather than
                        silently no-op'ing, so the dispatcher keeps the
                        delivery in flight and retries on heal (#5292).
  3. A2aCancelMidStream - cancel lands between PENDING and RUNNING; the
                        taskEpoch guard rejects stale-epoch late transitions
                        and the task converges on one terminal state (#5302).
  4. SubscriptionReRegisterAfterSplit - update during a Meta partition; after
                        heal the latest write wins, nothing dropped or
                        duplicated (#5288, #5301 SubscriptionStore).
  5. OffsetStoreRaceVsDeliveryStore - cross-thread offset-advance vs retire
                        race; the probe log proves every DELIVERY_REMOVE is
                        preceded by an OFFSET_WRITE at the same offset
                        (#5289 at-least-once).
  6. A2aDispatchRaceVsTaskStore - two dispatchers race on one task record;
                        stale-epoch writes are rejected and createTask yields
                        exactly one winner (#5291).

Every scenario runs in-process and deterministically. The JvmCrashHarness
from the previous commit remains the optional cross-JVM verification path.

Note on the taskEpoch contract exercised by scenarios 3 and 6: the epoch is set
at createTask and never reset, so updateStatus rejects any epoch that differs
from the record's. Same-epoch writes are last-writer-wins by design - the
Runtime dispatcher is the sole writer and the epoch guards against a restarted
instance's stale handle, not against intra-JVM ordering.

* docs(architecture): add §13.2.12 cross-store fault-injection (issue #5314)

Documents the 6-scenario CrossStoreFaultInjectionTest: why single-store contract
tests cannot cover cross-store invariants, the four harness primitives, the
per-scenario assertion table, why Testcontainers is not used, and the two
extension points (RocksDB-backed crash scenario, MetaBackedOffsetStore going
active).

Numbered 13.2.12 because §13.2.11 was taken by the dual-topology matrix in
PR #5317.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Architecture Review][P0] PARTITION_OWNED_PULL delivery topology (reopens #5300 second mode)

1 participant