Repository navigation
feat(cluster): acknowledge document writes at a quorum of copies - #2303
Merged
Merged
Conversation
Part of #1170. A write on a shard with two replicas waited for both. One replica that did not answer turned an otherwise stored document into HTTP 503, even though the primary and the other replica had it. The quorum is a majority of the copies placed at create (copies / 2 + 1). The primary already counts, so one of two replica confirmations is enough. The replica that misses that write is removed from the in-sync set by a Raft entry before the caller is acknowledged, and later reads do not use it. The placed count does not shrink, so the primary alone is still not a quorum of three copies. number_of_replicas 0 or 1 is unchanged. Single-node mode never enters this path. Nothing replays the writes a dropped replica missed. Snapshot install, wait_for_active_shards, and replica promotion stay open. Co-authored-by: xerj-org <xerj-org@users.noreply.github.com>
CHANGELOG.md was the only conflict. Both Unreleased Added entries are kept: the quorum document-write acknowledgement from this branch, and the DeepSeek Harness profile bundle that landed on main. Co-authored-by: xerj-org <xerj-org@users.noreply.github.com>
Owner
Author
|
Xerj Merger made the fix: document writes in a cluster are now acknowledged once a majority of the placed copies (primary included, copies/2+1) confirm, instead of requiring every replica; one slow or missing replica no longer turns a quorum-held write into a 503. Adds cluster tests and updates the clustering docs. Part of #1170 (issue stays open). |
cursor Bot
pushed a commit
that referenced
this pull request
Oct 24, 2026
Motivation: #2308 (semantic wildcard skip) and #2303 (cluster quorum acks) landed on main after this branch, and the changelog also picked up the #1122 large-JSON ingest bound. The branch no longer merged cleanly. Resolution: keep both Unreleased Fixed entries — the proxy semantic resume (#2306) and the large-JSON bound (#1122). The Rust overlap in xerj-autoindex and xerj-engine auto-merged. The proxy-width resume patch is unchanged against main, and main's quorum, wildcard-skip, and ingest-bound behavior is unchanged. No .github files were edited. Co-authored-by: xerj-org <xerj-org@users.noreply.github.com>
cursor Bot
pushed a commit
that referenced
this pull request
Oct 24, 2026
Nine pull requests landed on main after the rc.99 cut. They are cited in [1.0.0-rc.99] - 2026-10-23, and [Unreleased] stays empty. - #2245 fixes #2234: _refresh no longer waits on a background merge. - #2250 fixes #1612: autoindex estimates neural embedding time, and docs say --embed-mode is fixed at ingest. - #2296 closes #2294: a persisted index.default_pipeline runs again after a clean restart. - #2298: DeepSeek Harness profile bundle in dsh/, README links, and landing/brandbook/xerj-mark-400.png. dsh/package.json is 1.0.0-rc.99. - #2300 and #2301: Cline is marked tested in the EN/KO/ZH README tables, with a field report. The run was against v1.0.0-rc.98. - #2302 improves #1122, which stays open: a bulk reserves memory before parsing, and uncompressed JSON over 64 MiB is streamed. - #2303 is part of #1170, which stays open: a document write is acknowledged once a quorum of copies confirms. - #2308 fixes #2305: a semantic query over a wildcard, alias, or comma list skips indices that have no semantic_text field. The window since v1.0.0-rc.98 is 91 merged pull requests. The 18 crate versions in engine/Cargo.lock stay 1.0.0-rc.99. Landing footers that this merge touched stay V1.0.0-RC.99. /product now says rc.99 lists xerj hub next to xerj ask when the build has a hub signing key (#2282); the rc.98 capture of that page is otherwise unchanged. #2312 merged to corpus-hub, not main, so it is not in this cut. Co-authored-by: xerj-org <xerj-org@users.noreply.github.com>
Merged
cursor Bot
pushed a commit
that referenced
this pull request
Oct 24, 2026
The only conflict was CHANGELOG.md. engine.rs and landing/llms-full.txt auto-merged. Both behaviours stay. #1981 (this branch): a flat RSS at or above the watermark releases the breaker after 2 seconds and does not re-arm inside the 4 MiB band. While that latch stays engaged, drain requests double from 2 seconds up to 15 minutes unless RSS falls by more than 4 MiB. corpus index reports eta_quality=stalled at 0/N and after 10 minutes of silence on a phase with no denominator. main, kept alongside that: - #2296 / #2294: an existing index's persisted index.default_pipeline is copied from settings.json back into the in-memory map on boot, failed-index retry, and snapshot restore. - #2303: a single-document write is acknowledged at a quorum of copies, with the replica-sync lock on the engine. - #2250 / #1612: neural autoindex prints an estimated embedding time, and --embed-mode stays fixed at ingest. llms-full.txt keeps that text and the 0/N stall sentence. - #2308 / #2305 and #2311 came in with no overlap on the drain schedule. CHANGELOG keeps the #1981 entry and main's #2294 default_pipeline entry. No .github file was edited. Verified (rustc 1.97.1 (8bab26f4f 2026-07-14), from engine/): - cargo +1.97.1 fmt -p xerj-engine -p xerj-autoindex -- --check: exit 0 - RUST_MIN_STACK=32MiB cargo +1.97.1 test --no-fail-fast -j 2 -p xerj-engine -p xerj-autoindex -- --test-threads=2: exit 0, 3118 passed, 0 failed, 29 ignored. Includes flat_rss_above_the_watermark_releases_and_does_not_rearm, drain_requests_back_off_when_rss_does_not_fall, rss_noise_around_a_flat_footprint_still_releases, commit_starvation_holds_a_flat_footprint_until_commit_returns, and a_phase_at_zero_reports_a_stall_once_nothing_completes. Co-authored-by: xerj-org <xerj-org@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes, and why
Part of #1170. This is the quorum-ack half of step 2. It does not finish the issue.
On current main, before this branch:
index.number_of_replicasat create (feat(cluster): replicate acknowledged document writes onto shard replicas #2287). The caller was not acknowledged until every placed replica confirmed. One replica that did not answer was HTTP 503replica_not_acknowledged, even when the primary and another replica had the write. That is not a quorum.GET /{index}/_doc/{id}and a single-index search is already in (feat(cluster): route document writes and search to shard primaries #2271, feat(cluster): replicate acknowledged document writes onto shard replicas #2287). Search still does not merge aggregations, suggest, collapse, rescore, profile,search_after, or scroll, and an unscoped or wildcard search stays on the node that received it.FileRaftLog. A joining or restarted node does not catch up.WalReplicator'sReplicationMode::Quorumis a majority of the replica list alone (replicas.len() / 2 + 1). On two replicas that is both of them, so wiring it would not have changed a 3-node ring. The document path uses a majority of the copies placed at create:copies / 2 + 1, and the primary already counts.With two replicas, one confirmation is enough. The replica that does not confirm is removed from the in-sync set by
ClusterCommand::SetShardInSyncbefore the caller is acknowledged. Later GET and search failover use that set, not the create-time list. The placed count does not shrink, so the primary alone is still not a quorum of three copies. A replica that fell out is not added back: nothing replays the writes it missed.number_of_replicas0 or 1 is unchanged.[cluster] enabled = falsereturns before any of this. There is still nowait_for_active_shards, no replica promotion, and no snapshot catch-up.What remains for #1170
wait_for_active_shards.number_of_replicasafter create does not move or backfill copies._bulk,_update, update-by-query, and native/v1ingest do not route or replicate.search_after, or scroll. Unscoped/_search, comma lists, and wildcards stay on the receiving node.Evidence
a_quorum_of_copies_acks_when_one_of_two_replicas_does_noton an in-process 3-node ring withnumber_of_replicas: 2:PUTis 503replica_not_acknowledged(the primary alone is not a quorum).PUTsucceeds. The live replica store has the document. The stopped replica's store does not.GETandmatch_allfrom the stale member return the document with_indexorders.The same test failed while
replicate_writereturned on the first replica error, which is the previous behaviour:Restoring the quorum tally made that test pass again (0.20s). The existing ring tests still pass, including
number_of_replicas: 1(that one replica must confirm) and cluster disabled (the document stays on the receiving node, no replica store, search total 1).No benchmark was run. Single-node requests return before this path because
has_cluster()is false. That is a control-flow claim, not a measured latency.a_quorum_of_copies_acks_when_one_of_two_replicas_does_notnumber_of_replicas1 still needs that replica; single-node stays localan_acknowledged_write_is_on_the_replica_and_readable_after_the_primary_stops,cluster_disabled_create_stays_on_the_node_that_received_itChecks
cargo fmt --all(CI runsrustfmt --checkandclippy -D warnings)cargo build --release -j 32 -p <crate>cargo test -p xerj-cluster,cargo test -p xerj-engine --lib,cargo test -p xerj-engine --test shard_router_write_path, andcargo test -p xerj-api --test cluster_metadata_replicatesengine/tests/es-compat-yaml/yaml/CHANGELOG.md,landing/docs/clustering.html)docs/CONTRIBUTION_REVIEW.mdNot run: a release build,
cargo test -p xerj-apibeyondcluster_metadata_replicates, and the ES-YAML suite. The suite boots one node with cluster disabled; the new branch returns before routing when no handle is attached. No new YAML case, because the behaviour needs a ring. A real TCP three-node boot was not run; the ring test is in-process. The path where a quorum of replicas confirm but the in-sync Raft proposal then fails was not forced.Review notes: the effective diff is the replica ack tally and the in-sync publish. Single-node create and document writes still return before
replicate_write. A write is acknowledged only after the in-sync entry has been applied on the primary when the set actually shrinks. A 503 does not shrink the set. The in-sync set is invocation-local in the same way as the shard plan: the server Raft log is still in memory. Failure of the in-sync proposal does not roll back the primary write; the response stays 503 and a retry of the index is the recovery. No performance number is claimed.Provenance
_docor_searchfailure in the ES-YAML suite would falsify the "single-node unchanged" claim. NoCo-Authored-Bytrailer was added.