Repository navigation
Conversation
…pens within a round trip instead of at the deadline The MoveTo/Recycle paths drained by waiting for "no caller work" before replacing the connections. Under steady traffic that never happens: there is always a command in flight, so the drain waited out the entire announced window and recycled at the deadline, failing whatever was in flight at that instant and leaving no time to establish the replacement. Measured live on a Redis Enterprise endpoint rebind with ~450 ops/s on the connection: MOVING at +5.8s, MoveTo decided at +6.6s, connections replaced at +20.9s against a 15s window, 8 failed commands, and the "did not establish a replacement within the announced window" warning. The bridge now has a quiesce mode for the duration of the drain: new caller messages (single, batch, sync and async) go to the backlog instead of the socket, the backlog processor does not write while quiescing, and the drain waits only for commands already written. Held work is written to the replacement once it is established, as across any reconnect; a FailFast backlog policy no longer abandons it for a MaintenanceHandoff, which is a replacement we chose rather than a failure. Internal traffic (keep-alives) is not held. Same scenario with this change: MOVING +5.8s, handoff +6.7s, restored +6.8s, 0 failed, nothing outstanding at the recycle, no warning. The new test runs a steady load against the in-process server and asserts that the recycle follows the notification within a fraction of the window and that no command fails; it fails on main (recycle at ~10s, the whole window) and passes here.
|
Self-review notes (independent review pass over the diff, recorded here so they don't get lost). The quiesce/drain approach holds up in principle; the gaps are at the edges of the window. Six behavioral items and two doc items, roughly in severity order:
Items 3–8 are mechanical; 1 and 2 need a design decision (capped drain + |
…live caller work Review follow-ups on the quiesce-then-drain change: - The drain is capped at one second as well as by the window. A blocking or stuck command in flight would otherwise hold everyone else's work in the backlog for the rest of the window and still be failed at the end of it; past the cap the connections are replaced regardless, and whatever was still in flight fails as the server's close would have failed it. - The recycle takes the bridge's write lock and re-checks for in-flight work under it, so a writer that passed the hold check before it was set cannot slip onto the old connection between the check and the recycle. A writer that reaches the lock after a handoff recycled its connection queues for the replacement instead of writing to a dead connection; only for the handoff (an ordinary closure is still reported as one, #3167), never for a message bypassing the backlog, and never for internal traffic. - Held work goes through QueueOrFailMessage's queue branch, so QUIT is never queued and the duplication is gone; QUIT and internal traffic are not held at all, in the batch path as well as the single-message paths. - Work is held only while the connection is still live; once it has dropped, the ordinary disconnected rules and the backlog policy apply. - A held enqueue that lands just after the hold is released kicks the backlog processor itself rather than waiting for the heartbeat. - The orphaned summary is back on CheckBacklogForTimeouts, and docs/ServerMaintenanceEvent.md describes the hold. New test: slow commands in flight do not hold the drain past the cap, and traffic resumes on the replacement.
|
Addressed in bc52360, item by item:
Verification: Moving (11), Maintenance (169), Backlog, Connect, Issue3167 and the Release analyzer build all pass; full net10.0 suite 6876 total with the one known-flaky |
|
Self-review notes, round 2 (independent pass over the updated head,
Shape of the fix for 1/2/3/5: track the quiesce-start connection per bridge and only ever recycle that instance, under the lock on every path, and make the requeue path a |
…e lock, with a bounded wait; requeue raced writers at the head Second round of review follow-ups: - Each bridge records the connection current when the hold begins and only ever retires that instance (TryRetireQuiesceTargetAsync). A replacement established meanwhile is never touched however often the drain loop retries, which the previous pass could do while the other bridge was still draining; and if the target went away by itself there is nothing to retire. - Every retirement happens under the write lock: the forced pass at the cap as well. The lock wait is bounded by a cancellation token the drain owns (the cap plus a second of grace) rather than the mutex timeout; if the lock cannot be had even then, the connection is left to the server's close rather than recycled around a writer. - A writer that lost its connection to the handoff while waiting for the lock is requeued as held work (never failed under FailFast) at the head of the backlog, via a front queue every backlog operation drains first, so it keeps its place ahead of what the hold queued after it. - The cap is half the relaxed timeout (five seconds by default) bounded by the window, and timeouts are relaxed for the hold, so a blocking command with a timeout up to the cap completes and nothing held is timed out by the hold itself. A fixed second failed commands the server's close would have let finish; the whole window held everyone else's work for one of them. - Each bridge is released in its own try/finally and the processor wake is guarded, so a bridge disposed mid-handoff cannot leave the other quiesced. - The two unused in-flight predicates are gone; the requeue check is one helper at the three lock sites. Tests: a slow command that fits inside the cap is answered and the recycle follows it; a longer one is failed at the cap and traffic resumes on the replacement; the steady-load test is unchanged.
|
Round 2 addressed in the latest commit:
Verification as before: Moving (12), Maintenance (170), Backlog, Connect, Issue3167 and the Release analyzer build pass; full net10.0 suite 6877 with only the known-flaky failover test. Live with this revision: rebind MOVING +5.7s, handoff +6.5s, 0 failed; maintenance mode MIGRATING/MIGRATED/MOVING chain, handoff 0.6s after MOVING, 0 failed, both bridges involved. |
|
Self-review notes, round 3. Round 2's nine items all check out on the new head — per-bridge recycle tracking, the
Trajectory check: round 1 was design, round 2 was the new mechanisms' blunt edges, round 3 is memory-ordering and lifecycle corners — converging, but (1) and (2) are merge-blockers; (3)–(5) are small deliberate fixes; (6)–(8) are alignment/comment work. |
…ly at the next hold, per-bridge release, cap floor Third round of review follow-ups: - BacklogRequeueFront takes lock(_backlog) around the head insert. The timeout sweep peeks, then dequeues and compares under that lock; an unlocked insert between the two handed it a different message, which it treats as a threading bug, and the dequeued message was dropped uncompleted. - _recycledForHandoff is cleared when the next hold begins, not when the replacement establishes: the write lock is not fair, so a writer parked on it can be bypassed by the backlog processor long enough for the replacement to come up, and clearing then made it write to the disposed connection and tear down the healthy replacement. - DrainCap has a one-second floor, so a relaxed timeout of zero (relaxation disabled) does not force the recycle with commands still in flight. - Each bridge is released as its own retirement resolves, not when both have; the interactive bridge usually drains in a round trip and must not hold work while the subscription bridge takes longer. EndQuiesce is idempotent and kicks the processor unconditionally on the transition, closing the store-load race with a concurrent enqueuer. - The drain works on snapshots of the bridge fields, and the mutex take and release tolerate disposal mid-handoff. - A batch carrying a QUIT is not held at all, so the hold decision and the per-message action agree. - The lockDeadline comment no longer claims the mutex timeout plays no part. New test: the steady-load handoff with maintRelaxedTimeout=0.
|
Round 3 addressed in the latest commit:
Verification: Moving (13), Maintenance (171), Backlog, Connect, Issue3167 and the Release analyzer build pass; full net10.0 suite 6878 total, 0 failed this time (the flaky failover test happened to pass). Live: rebind MOVING +5.8s, handoff +6.1s, 0 failed; maintenance mode this run drained a node the proxy was not on, so MIGRATING/MIGRATED only, 0 failed, as expected. |
|
Self-review notes, round 4 (head
Pattern this round: rounds 1–3 were the hold's internal mechanics; these are its interactions with the pre-existing failure paths ( |
…work's timeout, budget from what is left Fourth round of review follow-ups: - A connection failure during the hold no longer abandons the held work under a FailFast backlog policy: that backlog is work diverted off a live connection, which would have been written and answered, so it is left for the replacement to flush. Previously only the handoff's own recycle was exempt. - The hold imposes its own floor on the effective timeout for its duration plus the reconnect (the cap, the lock grace and the connect timeout), independent of relaxation: with maintRelaxedTimeout=0 and a sub-second configured timeout, the one-second drain floor would otherwise have timed out exactly the work the hold protects. - The front requeue roots the multiplexer on the empty-to-non-empty transition, as the ordinary enqueue does. - The drain budget is what is left of the window after the decision has spent its share polling DNS, not the window minus jitter; with less than the floor left there is no hold, and the connections are replaced at once (still under the lock) so the replacement gets what remains. - The handoff marker is a short ring of the last four retired connections, never cleared, so a writer starved across back-to-back MOVINGs still recognises its connection. - Abandoning the backlog dequeues under the dequeuers' lock; a quiesced backlog pass returns before contending for the write lock; the processor start's wake tolerates disposal. - One hold predicate, shared by the single-message gates and the batch gate, which makes one pass over the batch. - The docs and the DrainCap remarks now agree with the clamp: the window bounds the hold because the server closes the socket when it ends.
|
Round 4 addressed in the latest commit:
Verification: Moving (13), Maintenance (171), Backlog, Connect, Issue3167 and the Release analyzer build pass; full net10.0 suite 6878 with only the known-flaky failover test (it passed on the previous run and failed on this one, as it does on unmodified main). Live: rebind MOVING +5.9s, handoff +6.4s, restored +6.5s, 0 failed. |
Problem
The
MoveToandRecyclehandoff paths drained by waiting for "no caller work" before replacing the connections. Under steady traffic that never happens: there is always a command in flight, so the drain waited out the entire announced window and recycled at the deadline, failing whatever was in flight at that instant and leaving no time to establish the replacement.Measured live on a Redis Enterprise endpoint rebind (
singleproxy policy,maintMovingEndpointType=Auto) with about 450 ops/s on the connection:The timing matched the code to the tenth of a second: 15s window, 0.8s jitter,
DrainThenRecycleAsyncwaiting out the remaining 14.2s.Change
PhysicalBridgegets a quiesce mode for the duration of the drain:HasCallerWorkInFlight), not for the backlog;FailFastbacklog policy no longer abandons it for aMaintenanceHandoff, which is a replacement we chose rather than a failure;The retirement path (
RetireAsync) already did the equivalent by marking the server unselectable first; that is not usable for a handoff on a single-endpoint database, hence the bridge-level hold.Tests
MovingUnderSteadyLoadRecyclesPromptlyAndFailsNothing: steady load against the in-process server, MOVING with a 10s window and a named successor; asserts the recycle follows within a fraction of the window and no command fails. Fails onmain(recycle at ~10s), passes here.Maintenance*(168),*Backlog*,*Connect*,RetirementUnderMaintenanceTestsall pass against the docker topology. Full net10.0 suite: 6875 total, 1 failure,FailoverTests.SubscriptionsSurvivePrimarySwitchAsync(primaryChanged >= 12got 10), which fails the same way on unmodifiedmainon this machine (1 pass in 4 runs), so not from this change.endpoint_rebind.