Skip to content

feat(consensus): persist retry receipts and shared sessions - #4387

Open
hubcio wants to merge 9 commits into
masterfrom
feat/durable-protocol-and-sessions
Open

hubcio wants to merge 9 commits into
masterfrom
feat/durable-protocol-and-sessions

Conversation

@hubcio

@hubcio hubcio commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Lost replies and capacity eviction could erase retry evidence and allow duplicate mutations after promotion or restart. This PR retains the original result by session, group and request identity, and publishes receipt checkpoints before reclaiming the corresponding WAL. Retries return the first receipt even when their payload changes, while preserving principal, session and operation checks.

Shared sessions use client-owned binding proof, authenticated lease renewal and ordered retirement across every allocated partition group. Retirement reports carry the swept namespace revision, so reports collected before a namespace change cannot finalize protection. Missing or corrupt retry history refuses recovery before destructive repair.

Persisted singleton acknowledgements wait for the WAL barrier. Purge/restart preserves the original send receipt without restoring purged messages. SDK reconnects retain encoded request identity, and later refusals cannot turn an uncertain request into a fresh mutation. Checkpoint publication runs through persistence completion; HTTP moves owned bodies and avoids unrelated session/gate scans.

Compatibility and limits:

  • Current and minimum binary protocol are 0.11.1. Intermediate development builds advertising that version may have incompatible layouts; matching clients and servers deploy together. Server peers require the same protocol, release and executable artifact, including identical stripping/build output. Mixed builds and rolling upgrades are unsupported.
  • This changes storage formats. Unsupported existing directories are refused unchanged. The breaking:storage label waives the legacy-upgrade check; no migration or rolling-upgrade adapter is supplied.
  • Missing/empty storage and new clusters initialize automatically. Total metadata-directory loss is outside automatic recovery guarantees. Retained metadata still fences an existing partition whose history is missing.
  • Ordinary SDK writes to Replicated topics remain supported. Crash-safe receipt recovery requires Persisted sends or Persisted/Quorum explicit offsets. HTTP ack=none confirms dispatch only. A detached reply waiter retains the same-session/partition lane until the reply or bounded deadline; queued writers acquire admission permits after that gate.
  • Public Rust ConsensusSession::bind and next_request_id now return Result; register_request_id preserves registration identity. VsrSessionControl adds required session_identity and session_bind_secret methods for external transport implementations; its public marker is not a seal. The proof accessor is hidden from generated docs and must stay private to session control. Explicit disconnect clears remembered login, while configured AutoLogin can authenticate a subsequent connection.
  • Rust producers create fresh retry attempts only after TransientNotAccepted or proven presubmission NotConnected/CannotEstablishConnection. HTTP failures use separate transport retry settings. Uncertain and terminal failures return the unconfirmed batch. An application resend can duplicate an earlier uncertain mutation. Authorized receipt access after permission revocation and general routing/cancellation APIs remain follow-up work.

clients_table_max and dedup_clients_max are fixed by the first committed prepare in each group. Recovery and transfer preserve that limit and warn on configuration mismatch. Live retry protection is not evicted; capacity becomes available after ordered retirement. Retirement remains per identity across every allocated group.

The bind proof is an independent session credential: password changes and PAT revocation/expiry do not end an established session. Binding rechecks owner existence and Active status; current permissions govern every operation. Logout, lease expiry and user deactivation end session access.

Go, Java, C#, Node and Rust-backed bindings use the coordinated login/binding contract. Established Go/Java/C# sessions now also resume coordinator connections with BindSession, retaining identity and sequence; only terminal 40/30 bind refusal permits fresh registration. Swift golden fixtures and error mirrors are regenerated from Rust.

Validation: the initial full just nextest gate passed 7,240 tests with 17 existing skips. This review round passed 2,731 affected Rust library tests with six existing skips, Node 353 unit tests/build/lint, Go 590 unit tests with the race detector/build/lint, C# 649 unit tests on each of net8/net10/build/formatting, and Java 1,179 non-integration unit tests. The affected Java uncertainty regression and static checks also pass after the final helper extraction. Required formatting, strict workspace Clippy, build, TOML checks and applicable hooks pass. Seven selected real-server tests pass, covering singleton/cluster lost receipts, purge, WAL reclamation, registry retirement, HTTP concurrency and repeated metadata snapshot installation. The previously published head has 107 passing CI checks and no failures; CI will rerun for these new commits.

Depends on merged #4365.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Oct 2, 2026
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.27492% with 477 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.03%. Comparing base (2d7fddb) to head (8e102ae).

Files with missing lines Patch % Lines
core/partitions/src/state_transfer.rs 76.26% 46 Missing and 29 partials ⚠️
core/metadata/src/impls/metadata.rs 90.51% 49 Missing and 12 partials ⚠️
core/sdk/src/websocket/websocket_client.rs 90.42% 34 Missing and 16 partials ⚠️
core/metadata/src/impls/recovery.rs 74.17% 29 Missing and 10 partials ⚠️
core/sdk/src/quic/quic_client.rs 88.78% 27 Missing and 9 partials ⚠️
core/server/src/boot/mod.rs 75.20% 16 Missing and 15 partials ⚠️
core/server/src/http/state.rs 71.08% 17 Missing and 7 partials ⚠️
core/server/src/consumer_group/liveness.rs 95.71% 15 Missing and 2 partials ⚠️
core/consensus/src/metadata_helpers.rs 93.30% 8 Missing and 6 partials ⚠️
core/sdk/src/tcp/tcp_client.rs 96.98% 11 Missing and 3 partials ⚠️
... and 26 more
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #4387      +/-   ##
============================================
+ Coverage     87.96%   88.03%   +0.07%     
- Complexity     1579     1606      +27     
============================================
  Files          1290     1290              
  Lines        230145   235659    +5514     
  Branches     193480   198654    +5174     
============================================
+ Hits         202440   207457    +5017     
- Misses        22980    23288     +308     
- Partials       4725     4914     +189     
Components Coverage Δ
Rust Core 89.13% <91.27%> (+0.06%) ⬆️
Java SDK 68.72% <ø> (-0.02%) ⬇️
C# SDK 77.83% <ø> (+0.21%) ⬆️
Python SDK 91.25% <ø> (+<0.01%) ⬆️
PHP SDK 85.67% <ø> (ø)
Node SDK 96.62% <ø> (+0.02%) ⬆️
Go SDK 70.30% <ø> (+0.11%) ⬆️
Files with missing lines Coverage Δ
core/binary_protocol/src/codes.rs 100.00% <ø> (ø)
core/binary_protocol/src/consensus/command.rs 100.00% <100.00%> (ø)
core/binary_protocol/src/consensus/header.rs 83.36% <100.00%> (-0.02%) ⬇️
core/binary_protocol/src/consensus/operation.rs 97.26% <100.00%> (+0.15%) ⬆️
core/binary_protocol/src/dispatch.rs 89.58% <100.00%> (ø)
...inary_protocol/src/requests/system/bind_session.rs 100.00% <100.00%> (ø)
...nary_protocol/src/requests/users/login_register.rs 98.70% <100.00%> (-1.30%) ⬇️
...ocol/src/requests/users/login_register_with_pat.rs 100.00% <100.00%> (ø)
...ry_protocol/src/responses/messages/poll_routing.rs 100.00% <100.00%> (ø)
...y_protocol/src/responses/messages/send_messages.rs 97.85% <ø> (ø)
... and 112 more

... and 39 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hubcio
hubcio force-pushed the feat/durable-protocol-and-sessions branch from 98a2f15 to 63eaf2a Compare October 2, 2026 22:32
@hubcio hubcio added the breaking:storage On-disk data format change - skips backwards compat CI label Oct 3, 2026
hubcio added 3 commits October 3, 2026 10:38
Lost replies and capacity eviction could erase retry evidence and
permit duplicate mutations after promotion or restart.

Persist original results by session, group and request identity.
Replay the first receipt even when a retry changes its payload, while
preserving principal, session and client-operation guards. Protect
committed capacity and publish receipt checkpoints before reclaiming
their WAL. Refuse recovery when protection is absent or corrupt.

Bind connections using client-owned proof, retain logical sessions
across reconnects, renew leases only on authenticated activity, and
retire protection across every allocated group before finalization.
Preserve uncertainty when a later attempt is refused.

Replay the exact encoded request after same-session resume and across
refused or unreachable roster peers within its deadline. Expired
sessions can log in again without replaying an ambiguous write under a
new identity. Keep HTTP attachments invalidatable and serialize writes
per partition under one deadline. Retain NoAck gates and reply slots
through resolution, clean cancelled registrations, and retransmit
retirement progress until all fences are acknowledged.

Restore recovered receipts before admission while allowing live
pipeline-backed commits. Allow intact empty WALs to elect, fence
missing history before replacement WAL creation, and compare bootstrap
revisions in the same domain.

Introduce protocol 0.11, mandatory storage admission and exact peer
build checks. Require explicit fresh-cluster initialization and
Persisted durability for explicit writes without changing defaults.
Align Go, Java, C# and Node login proofs and shared binding frames.
Validate unsigned binding epochs and expose explicit producer
durability in Rust and Python.

Cover changed-payload retries, reply loss, crash recovery and
reclamation, corruption, capacity pressure, lifecycle fences and
transport retries.

Document first-receipt integrity stamps and stable binding epochs.
Empty storage and Replicated writes were rejected despite existing
startup and SDK contracts. Restore automatic metadata initialization
and scope crash-safe retries to durable writes. Complete metadata
directory loss still requires verified restore.

Orderly shutdown must publish buffered WAL writes, and recovery must
checkpoint them before advancing reserved offsets. Retain repair
bodies across the gap and establish segment links before sends so
descriptor exhaustion cannot block the shutdown barrier.

Align replica, session and simulator fixtures, standalone formatting
and isolated Miri with the supported contracts.
Explicit QUIC and WebSocket disconnects reused remembered credentials.
Clear them like TCP while preserving involuntary transport recovery.

Python CI polluted empty storage with logs and expected terminal
errors to retry. Move logs beside data and align fixtures and docs
with the existing explicit non-admission retry contract.
@hubcio
hubcio force-pushed the feat/durable-protocol-and-sessions branch from 7264016 to a621d98 Compare October 3, 2026 08:38
Singleton offsets could acknowledge an undurable WAL entry, purge
recovery could replace the original receipt, and retirement could
count reports for an older namespace set. Session replacement and
later refusals also exposed retry identity and uncertainty gaps.

Wait for the singleton WAL barrier, preserve original receipt stamps
through purge, and fence missing partition history. Seal namespace
revisions into retirement reports and discard stale quorum evidence.
Queue checkpoint publication and reclaim only after completion.

Keep retained SDK requests within their session and preserve
uncertainty across later refusals. Fix explicit Node login after
confirmed expiry, first-boot marker ordering, lost metadata wakeups
and avoidable HTTP work.

Set current and minimum binary protocol to 0.11.1 and regenerate Swift
fixtures. Preserve automatic startup and Replicated writes; total
metadata-directory loss remains outside automatic recovery.

Document the public Rust API break: ConsensusSession bind and
next_request_id return Result. Producers start fresh retries only
after TransientNotAccepted; other failures return the unconfirmed
batch. Explicit disconnect requires a new login.

Validate with the full just nextest gate, affected foreign SDK suites,
strict Clippy, configured hooks and Miri peer-frame tests.
@hubcio

hubcio commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

/skill team-review-slim

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: Four experts reviewed the pull request at 75c26b5. The review found throughput changes on the HTTP write path, a fatal-vs-degrade contradiction in the partition commit walk, a misleading session error, stale foreign-SDK docs and guards, and one redundant journal barrier.

Counts: critical 0, warning 8, nit 4, simplification 1


This review was generated by Claude Code 2.1.284 on deepseek-flash[1m]. Review the output before you act on it.

Comment thread core/server/src/http/submit.rs
Comment thread core/server/src/http/submit.rs
Comment thread core/consensus/src/impls.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs
Comment thread foreign/node/src/wire/command.code.ts
Comment thread foreign/go/internal/command/code.go
Comment thread foreign/go/client/tcp/tcp_poll_routing.go Outdated
Comment thread core/journal/src/partition_journal/segments.rs Outdated
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Oct 3, 2026
Poll routes accepted malformed or mismatched bind replies, and the
Node protocol guard missed patch drift. Validate bindings before
polling and report both session identities on a resume mismatch.

Avoid capacity scans when queue bounds prove space and retain only
retirement entries during commit. Preserve receipt fences and HTTP
ordering without adding reservation counters. Remove a redundant
journal sync and correct session and protocol documentation.
@hubcio

hubcio commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

/skill team-review-slim

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: One critical finding: a state-transfer install leaves pending_retry_checkpoint stale, so the next receipt reclaim deletes the live receipt file and the partition fails to reopen with StateFileCorrupted. The review also finds a full-reply copy on the primary commit path and a session-bind error that names the wrong cause, plus smaller items on a duplicate-reply copy, a redundant segment sync, HTTP retry docs, Java protocol parity, and a constant binding.

Counts: critical 1, warning 2, nit 4, simplification 1

Findings without an anchor on a changed line:

  • core/partitions/src/iggy_partition.rs:1062 critical: After a state-transfer install, pending_retry_checkpoint still holds the pre-install frontier while checkpoint_op() has moved to the install op. This reclaim then deletes the receipt file the install wrote, so the partition fails to reopen with StateFileCorrupted. Reclaim from persistence.checkpoint_op(), or clear the field on every reset.
  • core/partitions/src/iggy_partition.rs:6636 warning: reply.clone() allocates an aligned frame and copies the whole reply for every committed client operation on the primary. Freeze the reply once and share it with the commit cache, copying only for an in-process waiter.
  • core/partitions/src/iggy_partition.rs:4573 nit: cached.into_message() copies the retained reply into an owned frame, and the bus branch then converts it back to Frozen. Send the cached Frozen on the bus path, and build the owned copy only for an in-process waiter.

This review was generated by Claude Code 2.1.284 on deepseek-flash[1m]. Review the output before you act on it.

Comment thread core/sdk/src/session.rs Outdated
Comment thread core/journal/src/partition_journal/segments.rs
Comment thread core/sdk/src/clients/producer.rs
Comment thread core/message_bus/src/replica/handshake.rs Outdated
hubcio added 2 commits October 3, 2026 19:44
Delayed persistence completion could delete the receipt checkpoint
installed by state transfer. Reclaim against the published frontier.

Share immutable replies on bus paths and avoid redundant retained-file
barriers while preserving recovery truncation durability. Correct SDK
session diagnostics and retry docs, and check Java protocol parity.
RequestIdExhausted was added to the Rust catalog without updating
Swift's mirror, failing the golden check. Regenerate the fixture and
add the matching Swift case so error-code parity stays intact.
.client_table
.borrow()
.ended_sessions()
.min_by_key(|identity| (identity.metadata_watermark, identity.client_id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical: retirement is O(ended sessions x partitions) of fsynced RetireSession ops, one identity at a time, including partitions the session never touched.
session_retired needs commit_min == commit_max, so busy partitions keep failing, the cursor resets and the sweep resends every ~200ms. A fatal or transferring partition on the primary node, or a TruncatePartition revision bump, stalls every finalize.
Tables never evict, so churn fills them and logins are refused cluster-wide.
Suggest one frontier op per partition covering all sessions ended at <= W, skipping tombstoned or fatal namespaces, and counting the primary like any other reporter.

Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/server/src/dispatch/partition.rs Outdated
Comment thread core/consensus/src/client_table.rs
Comment thread foreign/node/src/client/client.socket.ts
Comment thread core/sdk/src/quic/quic_client.rs Outdated
Comment thread foreign/csharp/Iggy_SDK/Vsr/VsrConnection.cs
Comment thread core/sdk/src/clients/producer.rs Outdated
Comment thread core/common/src/traits/binary_transport.rs
Comment thread core/sdk/src/clients/client.rs Outdated
Comment thread core/metadata/src/impls/recovery.rs
Comment thread core/partitions/src/state_transfer.rs Outdated
// a write refused transiently here is replayed after its
// successors committed. The slice therefore keeps a committed-id
// window under the watermark (`consensus::COMMITTED_WINDOW_BITS`)
// and admits an unmarked id inside it instead of absorbing it.
if !is_auto_commit_client(client_id) {
if consensus.pipeline_has_message_from_client_request(client_id, request) {
if let Some((pending_session, pending_request, pending_operation)) =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preflight replaced the exact-replay check pipeline_has_message_from_client_request with pending_request, so on the binary transports a pipelined second request from the same client in the group gets TransientNotAccepted. Pipelining clients such as the Java async SDK handle that code as a routing failure, retry on a timer and run leader rechecks for a lane that is only busy. The thread at core/server/src/http/submit.rs:370 covers the HTTP side effect of the double resolve. This one hits any pipelining binary client.

Comment thread foreign/go/client/tcp/tcp_session_management.go
Comment thread core/consensus/src/client_table.rs Outdated
Comment thread core/consensus/src/client_table.rs Outdated
Comment thread core/consensus/src/client_table.rs Outdated
Comment thread core/server/src/session_manager.rs Outdated
Comment thread core/sdk/src/session.rs
Comment thread core/sdk/src/poll_routing.rs
hubcio added 2 commits October 4, 2026 11:35
Retirement could race with queued writes and reuse obsolete quorum
reports. Transferred retry tables could also be checkpointed before
their effects were applied, making restart recovery fail.

Exclude writes while their session retirement is pending, qualify
reports by view, and checkpoint only after applied state catches up.
Retain one partition receipt, distinguish uncommitted transfer
capacity, and isolate corrupt receipt recovery to its partition.
Reuse resolved HTTP partitions and capture the admission frontier
before routing waits. Warm peer identity outside shard runtimes.
Transport loss reset established sessions in Go, Java and C#,
leaving old protection slots reserved and obscuring retry identity.
Java, Node and C# also let later refusals overwrite an unknown
mutation outcome.

Resume established sessions with their original proof and permit
fresh registration only after a definitive bind refusal. Preserve
uncertainty across later refusals, fence retained Rust frames after
transport locking, and restore retries for proven unsent attempts.
Document session and API contracts and synchronize error catalogs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking:storage On-disk data format change - skips backwards compat CI S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants