Repository navigation
Have the successor ready before it takes over - #99
Merged
jbrown9513 merged 5 commits intoSep 25, 2026
Merged
Conversation
A ramp upgrade's replacement loads every table its predecessor holds, read-only, while the predecessor still serves them: the dataset, its indexes and every flushed generation. When the predecessor releases a table the claim opens only the writer on what is already loaded. Nothing about ownership or fencing changes: nothing is claimed or written before the release. The release also carries the table's next arrival number, so the successor does not scan the table for its highest; warming a table no longer holds its writer; and reading the epoch probes two manifest versions past the hint, as the writer does, not 64.
… into engine/successor-preopen
A flush asked for from outside - an upgrade flushes every table before it starts the replacement - held the table's writer until its generation was written, so every write to the table waited out an object-store flush. It now freezes the memtable under the writer and waits for the flush without it; appends go on into a fresh memtable. Each release logs how long its flush and its record took, so a slow handover says which.
A handover that leaves a write waiting now logs the step it waited in - the route decision, the local write, or a forward and to whom - with the status and route error it ended with. The successor test also writes to the original directly, as a connection the proxy already holds does, and moves to the successor only when the original stops answering.
jbrown9513
marked this pull request as ready for review
September 25, 2026 01:12
release_key took the writer slot and then the arrival counter to record the next arrival number in the release. reserve_seq takes them the other way round: it holds the counter while its first seeding reads the table through the writer. A first write racing a hand-back therefore waited for the writer while the release waited for the counter, until the write's client gave up: stop_start saw a hand-back take 29.9 s (flush_ms=29956) and a write time out at 30 s, in 2 of 7 runs. The release now takes the counter first, as every write does, and still reads the number with both held.
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.
Stacked on #98.
Problem
On a ramp upgrade, the process being replaced released every table when it stopped. The replacement then opened each table from nothing, and every write to that table waited for the open. Per table on staging this took about 5 s after the release. Adding the original's own ~1.5–2 s release gave a worst write of 7–9.5 s per upgrade.
Staging logs broke that cost into four parts:
next_writer_epochprobed 64 shard-manifest versions past the hint.highest_seqran amax(_seq)scan over a cold table, about 2 s.The upgrade's pre-swap flush also held each table's writer until its generation was written.
Change
Ownership and fencing are unchanged: the successor claims and writes nothing before a release.
Prepared::refresh) and opens only the writer on it (Table::open_prepared).OwnerRecord.next_seqis written by the releasing owner while it still holds the writer. A claim from a release starts from it (seq=handedin the open log), so there is no table scan. Only a claim from a release inherits it, and only once.Table::warmercaptures what to open under the writer;Warmer::runopens it without the writer.Engine::flush(/flush_lsm, which an upgrade calls on every table first) freezes the memtable under the writer and waits for the flush without it (Table::seal).flush_msandelapsed_ms;load_ms,epoch_ms,record_ms,open_msandprepared=;walleye.route slow).I also tried releasing one key at a time to the successor (the local-only commit from the previous investigation). With prepared tables it was worse: the slowest takeover was 2.4 s against 1.6 s concurrent, in the same local setup. It is not in this PR.
Tests
tests/successor.rs: an original and its replacement (same node id).--ignored, needsminio): 0.9–1.3 s in a debug build. Before this change it was 1.4–1.9 s, and the 64-HEAD probe alone was 0.5–1.1 s at a 30 ms round trip.ownership::a_successor_knows_what_its_predecessor_holds_and_where_it_stopped: the predecessor's keys are seen, the handed number is inherited once, and a claim from anything but a release inherits nothing.cargo fmt --checkand clippy (-D warnings) are clean. walleye-lance and walleye-node tests pass: 36 test binaries.Staging
GET /internal/versiondigest was checked before its upgrade counted, and all 30 matched.prepared=true.The outliers. Each one coincides with object-store latency, not with the successor's open:
What is left is round trips in three places:
With normal object-store latency the whole handover stays under 5 s. Under a latency spike it does not.