Skip to content

fix(cluster): serve metadata reads at or above the client's own writes - #4024

Open
numinnex wants to merge 6 commits into
masterfrom
peer_shard_read_stale_state
Open

fix(cluster): serve metadata reads at or above the client's own writes#4024
numinnex wants to merge 6 commits into
masterfrom
peer_shard_read_stale_state

Conversation

@numinnex

@numinnex numinnex commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

A client that commits a metadata write and then re-homes its session onto a backup can be served the pre-write state. auth.rs already documents the gap: the session epoch is the register's commit op, and on a backup that forwarded the proposal the local applied commit still lags it. A backup applies committed ops only as its commit walk advances, and nothing tied a read to the op the client's own write committed.

Metadata reads now gate on the connection's committed watermark. One applied-frontier counter per process advances after every metadata apply and is shared with every shard, which for the first time gives a shard without consensus the applied position it had no way to observe. The watermark comes from the commit field replies already carry, seeded at bind from the session epoch. The fast path is a single atomic load with no awaits, so the shared-nothing read path is unchanged; a lagging node parks briefly, then fails the read retryable rather than answering stale.

Over HTTP this closes the forwarded-register case, where a healthy backup forwards the register so the bound epoch can exceed the local frontier. It does not close the case where forwarding is active: the follower relays the write, its handler never runs, so the node that later serves the read holds no session and no watermark. Closing that needs the serving primary's commit op to travel back to the reading node, for instance a response header beside the view the forward middleware already relays. That is additive but touches every control-plane write response, so it is left out here and documented at the gate.

The BDD delete-then-get steps now assert "not the stream we deleted" instead of "nothing at this id". The server hands a deleted stream's numeric id straight to the next create, so once scenarios share a server the old assertion cannot hold, and removing the polling loop without this would have left the spec flaky for an unrelated reason.

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

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.60887% with 113 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.41%. Comparing base (412014a) to head (456c38a).

Files with missing lines Patch % Lines
core/server/src/dispatch/mod.rs 23.80% 32 Missing ⚠️
core/simulator/src/lib.rs 89.76% 19 Missing and 3 partials ⚠️
core/server/src/dispatch/reads.rs 90.13% 10 Missing and 5 partials ⚠️
core/server/src/http/reads.rs 83.72% 9 Missing and 5 partials ⚠️
core/shard/src/metrics.rs 50.00% 9 Missing ⚠️
core/server/src/http/handlers.rs 85.18% 8 Missing ⚠️
core/server/src/responses.rs 82.35% 6 Missing ⚠️
core/server/src/http/forward.rs 78.94% 4 Missing ⚠️
core/metadata/src/applied_frontier.rs 98.77% 2 Missing ⚠️
core/server/src/session_manager.rs 98.21% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #4024       +/-   ##
=============================================
- Coverage     85.25%   68.41%   -16.84%     
  Complexity     1402     1402               
=============================================
  Files          1236     1236               
  Lines        182594   156098    -26496     
  Branches     148898   122405    -26493     
=============================================
- Hits         155664   106799    -48865     
- Misses        22886    45335    +22449     
+ Partials       4044     3964       -80     
Components Coverage Δ
Rust Core 64.47% <88.60%> (-21.66%) ⬇️
Java SDK 67.29% <ø> (ø)
C# SDK 76.33% <ø> (-0.10%) ⬇️
Python SDK 90.06% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 96.15% <ø> (-0.09%) ⬇️
Go SDK 69.35% <ø> (+0.03%) ⬆️
Files with missing lines Coverage Δ
core/metadata/src/impls/metadata.rs 88.72% <100.00%> (+0.23%) ⬆️
core/server/src/boot/mod.rs 87.04% <100.00%> (+0.14%) ⬆️
core/server/src/boot/threads.rs 76.66% <100.00%> (+0.11%) ⬆️
core/server/src/dispatch/submit.rs 97.43% <100.00%> (+0.88%) ⬆️
core/server/src/http.rs 92.88% <100.00%> (+0.01%) ⬆️
core/server/src/http/error.rs 87.35% <100.00%> (+0.34%) ⬆️
core/server/src/http/extractor.rs 85.24% <ø> (ø)
core/server/src/http/reply.rs 89.60% <ø> (-0.43%) ⬇️
core/server/src/http/session.rs 95.23% <100.00%> (-0.31%) ⬇️
core/server/src/http/state.rs 90.04% <100.00%> (+1.62%) ⬆️
... and 14 more

... and 330 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 left a comment

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.

two things I couldn't anchor to the diff:

  • responses.rs:241 is the shared consumer_group_memberships read behind GET_ME, GET_CLIENTS and GET_CLIENT, and the HTTP get_clients / get_client handlers reach it without read_local either. Same gap as the binary arms, on both planes.
  • dispatch/submit.rs:217 and responses.rs:1569 are the same Command::Reply guard plus try_from_bytes::<ReplyHeader> decode, and both fire on the same message in dispatch/mod.rs. One reply_header() helper in responses.rs covers both.

One thing I looked at and decided against: merging hold_for_frontier with await_recovery_barrier. They're bounded differently (poll count vs wall-clock deadline), and the merge would delete barrier_state's tests and turn the max_polls: 8 injection into a flaky wall-clock test. Worth taking only the one-line crumb - drop the local POLL const and reuse READ_FRONTIER_POLL.

Comment thread core/server/src/dispatch/reads.rs
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread bdd/java/src/test/java/org/apache/iggy/bdd/BasicMessagingSteps.java
Comment thread core/server/src/dispatch/reads.rs
Comment thread core/server/src/dispatch/submit.rs Outdated
Comment thread core/simulator/src/client.rs Outdated
Comment thread core/server/src/boot/mod.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 Sep 3, 2026
@numinnex

numinnex commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@numinnex
numinnex force-pushed the peer_shard_read_stale_state branch from 82db2b4 to 3d95831 Compare September 3, 2026 15:45
@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Sep 3, 2026
Comment thread core/server/src/http/submit.rs
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread core/server/src/http/reads.rs Outdated
Comment thread core/metadata/src/applied_frontier.rs Outdated
Comment thread core/server/src/dispatch/reads.rs
Comment thread core/metadata/src/applied_frontier.rs Outdated
Comment thread core/server/src/dispatch/reads.rs Outdated
Comment thread core/simulator/src/lib.rs Outdated
Comment thread core/server/src/session_manager.rs
Comment thread core/server/src/dispatch/submit.rs
@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 Sep 3, 2026
@numinnex

numinnex commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Sep 3, 2026

@hubcio hubcio left a comment

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.

pr description paragraph 3 still says the forwarding-active case is not closed - "that is additive but touches every control-plane write response, so it is left out here and documented at the gate". 456c38a closes it in this same pr via the applied-op header. worth updating before merge, since the body lands in the squash commit message.

the two comments below are the same staleness in the code docs.

// This node's own view only: on a follower with HTTP forwarding ON the
// write is relayed to the primary by the middleware and never reaches
// this task, so the follower's floor stays where the register left it
// and its read-your-writes guarantee is the register epoch's. Closing

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 last three lines are stale as of this branch. forward.rs now copies iggy-applied-op off the relayed response and record_relayed_floor records it as this caller's floor, so the primary's commit op does come back with the relayed response. a future reader lands here and concludes the forwarding hole is still open.

one caveat that does survive and is worth keeping in the rewrite: classify_reply relays a 503 carrying TransientNotCommitted instead of retrying it, and record_relayed_floor only records on 2xx - so a write that did commit behind that code still leaves no floor. that's the honest version of what this comment is reaching for.

///
/// Scope is this node's own view. A user whose write this node relayed over
/// HTTP, or who wrote through a different node entirely, left no floor here;
/// closing that needs the serving primary's commit op to reach the reading

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.

first clause is stale - a write this node relayed over HTTP does leave a floor now, record_relayed_floor records the serving primary's applied op off the response header. second clause still holds: a user who wrote through a different node entirely leaves nothing here.

something like:

/// Scope is this node's own view. A write this node RELAYED leaves a floor
/// here: the relay records the serving primary's applied op off the response
/// (see `http::forward::record_relayed_floor`). A user who wrote through a
/// different node entirely still leaves none.

@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 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants