Skip to content

ADR-0026: uncertain commit verification and replay contract

Status: accepted for the specified single-instance scope. Full hosted run 38142030670 verifies twelve parser cases and five live commit-cut/receipt/cancellation cases; focused run 38142624691 at 3003979 passes all six live cases, including actual transient deadlock, plus 26 relevant parser/protocol cases. See the recovery guide and tracking ledger (maintainer-local record) for evidence.

FS-06 requires actual disconnects around commit, transient/cancellation boundaries and safe retry behavior. Existing SaveChangesFailureTests verify client state using scripted transport responses. Existing migration checkpoints and BloggingCancellationProxy verify different boundaries; none establishes a lost commit acknowledgement over a real connection. Keep those distinctions in the published evidence.

Existing runtime contract

Neo4jDatabase.SaveChangesAsync owns a transaction for an ordinary save, executes the complete mutation plan and publishes generated values only after commit and cleanup return. Neo4jContextTransaction does not retry or attempt rollback after commit throws, because the server may already have committed. EF entries remaining pending do not establish whether the graph changed. A successful commit followed by cleanup failure is also distinct from an uncommitted save. Explicit transactions can accept EF state before the caller commits; disposing the context is necessary when reconciling an uncertain outcome.

These runtime observations are now backed by the live acceptance below as well as the existing protocol tests.

Implemented live fault harness

Use one bounded, loopback-only TCP proxy in the existing serialized integration suite. Its sole upstream must be the already marker-verified disposable fixture on 127.0.0.1:17687. It creates no additional database or container. An isolated driver connects to its ephemeral loopback port; all verification and cleanup use the original fixture driver.

Arm the proxy at the actual driver's transaction CommitAsync entry, after all save result cursors have been consumed. A forwarding interface wrapper may provide this boundary but must not simulate the database outcome. Before arming, forward handshake/authentication and data traffic without logging or retaining payloads. After arming, validate the bounded Bolt COMMIT message and test two distinct cuts:

  1. Disconnect before forwarding COMMIT. Verify the real graph has no committed effect and the provider leaves entries pending without retry.
  2. Forward COMMIT, receive its complete SUCCESS frame, then disconnect before forwarding that acknowledgement to the client. Assert the harness actually observed SUCCESS and independently verify the committed graph state from a fresh connection. The provider must surface failure, retain pending EF state and avoid automatic replay.

The parser must handle fragmented reads, chunk framing and no-op chunks, bound message sizes and lifetime, fail closed for unexpected message shapes, and close only its owned sockets. Tests must not infer “after commit” from elapsed time, a sent COMMIT or a generic connection error. No raw credentials, Cypher values, Bolt buffers or response metadata may enter logs/artifacts. Parser/control tests precede live use. Pinned Driver/Bolt behavior must be checked against actual source and wire observations.

Protocol source checked 2026-10-11: the official Bolt message specification defines two-byte big-endian chunk lengths, zero-length message boundaries and keep-alive no-op chunks. COMMIT has signature 12 and no fields; its SUCCESS response confirms explicit-transaction completion. The harness must correlate this response to COMMIT, not confuse it with RUN acceptance or another message's SUCCESS.

Application operation receipts

Demonstrate an application-owned receipt with a caller-stable operation ID and payload fingerprint, protected by a unique application-key constraint and created in the same atomic save as its effects. This is an explicit application pattern, not a provider promise of automatic exactly-once execution.

  • A receipt with the expected fingerprint, observed through a fresh context, confirms the corresponding atomic operation committed. A different fingerprint is an operation-ID conflict.
  • An absent receipt is not by itself proof of rollback: the original transaction may still be in flight or the observation may be stale.
  • Any deliberate retry must reuse the same receipt identity and fingerprint. A uniqueness conflict must abort the entire retry, including effects that executed before receipt creation. Reconcile afterward; never turn every constraint failure into success.
  • Verify with a real counter/version change, not merely an idempotent SET. Exercise competing attempts and prove exactly one operation's effect persists and no unrelated mutation is accepted.
  • External side effects are outside the graph transaction. This pattern cannot certify email, payment or arbitrary remote effects. Cluster routing/bookmark uncertainty remains an explicit boundary of single-instance acceptance.

Accepted evidence and limits

The focused gate retains existing scripted failure tests and adds deterministic real in-flight boundaries: a server-observed lock wait during cancellation and an actual two-transaction deadlock. Document whether a failure occurred before work, during a command, during commit, or after acknowledged commit/cleanup, and require fresh-context reconciliation where appropriate. Publish precise commands/results, actual wire cut observations, final graph state and limitations. Accepted tests and the application guide close FS-06 for this scope. No automatic retry policy, exception reclassification or production recovery API was added; the existing runtime contract passed unchanged. Both hosted gates report zero warnings/errors and remove their exact owned fixtures. Cluster failover, external effects and production operational validation remain outside this single-instance fault profile.