Phase 2 follow-up — Cross-store fault-injection E2E (#5301 Sub-PR D)
Source: Issue #5301 — Sub-PR D, deferred at issue close.
Severity: P1 (reliability correctness; not on the critical path for A2A / Runtime merge)
Blocks: — none directly; motivates future changes to DeadLetterStore / TaskStore / DeliveryStateStore
Why this is split out
The unified state control-plane stores (OffsetStore / SubscriptionStore / DeliveryStateStore / SessionStore / DeadLetterStore / TaskStore) were landed as separate SPI + concrete backends across PRs #5310 (Sub-PR A), #5311 (B), #5312 (C). Each sub-PR carried its own unit tests, but the six stores have never been exercised together in a fault-injection harness. Sub-PR D was the original plan for that.
Scenarios to cover
All six scenarios target the develop branch as of apache/eventmesh#5313 merged.
-
Crash mid-ACK → re-ACK on restart
- Setup: submit a message; pause the dispatch loop just after
DeliveryStateStore.put but before the connector ACK returns.
- Inject: hard kill the JVM.
- Expectation on restart:
ReliableDispatcher.recover() re-enqueues the same deliveryId; consumer sees exactly one delivery.
-
Meta partition during DLQ transition
- Setup: force a
DLQ write; pause the connector ack callback.
- Inject: kill the Meta client (close socket / 30s pause).
- Expectation:
DeadLetterStore.recordDeadLetter is the durable gate; the source delivery is NOT retired until the Meta write is acknowledged. After Meta recovers, the delivery is retired.
-
A2A task cancelled mid-stream
- Setup: start a long-running A2A
TaskRecord (e.g. via A2AGatewayService.submitTask against a slow agent).
- Inject:
cancelTask(taskId) while the agent is mid-SSE.
- Expectation:
TaskStore.updateStatus(..., Status.CANCELED, ...) succeeds; pending SSE subscribers get a terminal CANCELED event; the agent receives a cancel signal.
-
Subscription re-register after split
- Setup: a consumer group is registered via
SubscriptionStore.
- Inject: Meta cluster split for 30s; two Runtime instances see different Meta views.
- Expectation: when the split heals, both instances converge to the same subscription set; no duplicate redelivery for already-ACKed messages.
-
Offset-store race vs. delivery-store recovery
- Setup: offset store and delivery state store are co-located but updated independently.
- Inject: after a crash, recover delivery state first, then offset state.
- Expectation: ACKs that landed between the two writes are honored exactly once.
-
A2A dispatch race vs. TaskStore state
- Setup: an A2A agent is registered; a task is submitted through the Runtime dispatcher.
- Inject: invoke
cancelTask exactly at the moment the dispatcher dispatches the task to the agent.
- Expectation: outcome is one of (delivered then canceled) or (canceled before dispatch) — both observable; final
TaskRecord.status is CANCELED; DeadLetterStore does NOT receive a record for the canceled task.
Test harness
- Use Testcontainers + JUnit 5 (Java 21) — pattern established by the
Runtime module's existing fault-injection tests in eventmesh-runtime/src/test/java/.../cluster/ClusterDeliveryFaultTest.
- For Meta: spin up the in-process Nacos-compatible Meta (
InProcessMetaService) already used by the unit tests, plus an optional Meta kill-switch (close + reopen the socket) for partition scenarios.
- For the JVM crash scenario: use a child-process harness (
ProcessBuilder + Runtime.exec) so the test can SIGKILL the runtime under test and re-launch it.
Acceptance
- All six scenarios have a JUnit test that reliably reproduces the failure mode (5/5 runs in CI).
- A short doc note (
docs/architecture/state-stores-fault-injection.md) lists the scenarios and the property each one asserts.
Out of scope
- Performance / load (separate issue if needed).
- Production fault-injection tooling.
- Changes to the store APIs themselves — this is purely verification.
References
Phase 2 follow-up — Cross-store fault-injection E2E (#5301 Sub-PR D)
Why this is split out
The unified state control-plane stores (
OffsetStore/SubscriptionStore/DeliveryStateStore/SessionStore/DeadLetterStore/TaskStore) were landed as separate SPI + concrete backends across PRs #5310 (Sub-PR A), #5311 (B), #5312 (C). Each sub-PR carried its own unit tests, but the six stores have never been exercised together in a fault-injection harness. Sub-PR D was the original plan for that.Scenarios to cover
All six scenarios target the develop branch as of
apache/eventmesh#5313merged.Crash mid-ACK → re-ACK on restart
DeliveryStateStore.putbut before the connector ACK returns.ReliableDispatcher.recover()re-enqueues the samedeliveryId; consumer sees exactly one delivery.Meta partition during DLQ transition
DLQwrite; pause the connector ack callback.DeadLetterStore.recordDeadLetteris the durable gate; the source delivery is NOT retired until the Meta write is acknowledged. After Meta recovers, the delivery is retired.A2A task cancelled mid-stream
TaskRecord(e.g. viaA2AGatewayService.submitTaskagainst a slow agent).cancelTask(taskId)while the agent is mid-SSE.TaskStore.updateStatus(..., Status.CANCELED, ...)succeeds; pending SSE subscribers get a terminalCANCELEDevent; the agent receives a cancel signal.Subscription re-register after split
SubscriptionStore.Offset-store race vs. delivery-store recovery
A2A dispatch race vs. TaskStore state
cancelTaskexactly at the moment the dispatcher dispatches the task to the agent.TaskRecord.statusisCANCELED;DeadLetterStoredoes NOT receive a record for the canceled task.Test harness
Runtimemodule's existing fault-injection tests ineventmesh-runtime/src/test/java/.../cluster/ClusterDeliveryFaultTest.InProcessMetaService) already used by the unit tests, plus an optional Meta kill-switch (close + reopen the socket) for partition scenarios.ProcessBuilder+Runtime.exec) so the test can SIGKILL the runtime under test and re-launch it.Acceptance
docs/architecture/state-stores-fault-injection.md) lists the scenarios and the property each one asserts.Out of scope
References