Repository navigation
driver/internal/eventloop: RegisterConnWait stops at a done ctx even when its pause has ended too - #1140
Merged
Conversation
… done before the wait (Refs #1104) The wait's pause ends in a select on ctx.Done() and the pause's timer. When both are ready Go picks at random, so a ctx done before the call can still be followed by a second RegisterConn. The test makes the timer ready at once (a first pause of 0, through registerConnWait, which RegisterConnWait calls with its 100 us) and fails: 968 of 2000 waits made the second call. The next commit fixes it.
…when its pause has ended too (Refs #1104) The select after a pause picks at random when the goroutine was not run for the whole pause and ctx is done as well, so a ctx that was done before the call could still be followed by a second RegisterConn, registering the fd for a caller that had given up. Look at ctx.Err() after the select, whichever case fired. The budget path needs no such priority: it is checked after a call, and the call at the deadline is the last try by design.
|
Caution Review failedThe pull request is closed.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…cancel during the wait and a passed deadline are tested (Refs #1104) The doc said a done ctx is never followed by another call; it is a ctx found done when a pause ends, and a ctx that was done on entry still gets the first call. Two more deterministic cases: the first refusal cancels the ctx (pause 0) and a ctx whose deadline has passed; each asserts one call and errors.Is for the refusal and the ctx error. Comments: the pause reaches 6.4 ms, a pause of 0 never grows, and the budget error's stale comment.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Flakes lane, package H:
RegisterConnWaitcan make a secondRegisterConncall for a ctx that was done before the call. A real race in the code (#1129), found as a CI failure of #1137. Refs #1104The bug
RegisterConnWaitwaits out the io_uring engine's "fd already registered" refusal: after the first refusal it pauses (100 us, doubling) and callsloop.RegisterConnagain. The pause isWhen the goroutine is not run for the whole pause, both channels are ready when the select runs, and Go picks one at random. So a ctx that was done before the call is followed by another
RegisterConnabout half of the time in that case: an fd registered with the engine for a caller that gave up. The documented contract is that a done ctx stops the wait.Found in #1137's CI, required check "Portable fallback (macos-latest)", https://github.com/goceleris/celeris/actions/runs/40485540133 (job 122126282588):
TestRegisterConnWait887/ctx_done_returns_the_refusalfailed withregister_wait_887_test.go:78: RegisterConn calls = 2, want 1. It is not a test flake: the test is right and the code is wrong.The fix (commit 2)
After the select, whichever case fired, look at
ctx.Err()and return the same wrapped refusal (%w (gave up waiting: %w)with the ctx error) before callingRegisterConnagain. The budget path needs no such priority: it is checked after a call, and the call at the deadline is the last try by design. The doc ofRegisterConnWaitsays what holds: a ctx that is done when a pause ends stops the wait before the nextRegisterConncall, and a ctx that was already done on entry still gets the first call (the wait is only for a refusal of it, and the drivers close the fd on a failed handshake). A ctx cancelled in the few nanoseconds between the check and the call is followed by that one call; that is inherent.The test (commit 1, fails on its own)
A deterministic version of the race needs the timer to be ready when the select runs, which the scheduler decides.
registerConnWaitisRegisterConnWaitwith the first pause as a parameter (RegisterConnWaitpasses the 100 us, behaviour unchanged); the test passes 0, so the timer is ready at the select, and checks 2000 waits with a ctx cancelled before the call that no wait made a second call. Two more cases (commit 3): a ctx cancelled DURING the wait (the scripted loop's first refusal cancels it, pause 0; 2000 waits, exactly one call each,errors.Isholds for both the refusal andcontext.Canceled), and a ctx whose deadline has passed (context.DeadlineExceeded, 200 waits).golang:1.27.3,--cpus 2,-race -count=20:TestRegisterConnWait887(5 cases) andTestIsNumberBusy887pass 20 of 20 too; the wholedriver/internal/eventlooppackage passes once with-race-race -count=20; the whole package passes once with-raceThe same pattern elsewhere in
driver/andinternal/I grepped the non-test code for
selectcases on a timer or ticker channel (<-t.C,<-ticker.C,<-time.After) and read each one for a side effect that follows a done context or close channel that was ready too:driver/redis/pubsub.go:556(reconnect backoff vscloseCh) anddriver/redis/sentinel.go:264(subscribeLoop): same shape, already safe:ps.closed.Load()/s.closed.Load()is checked after the select / at the top of the loop before anything is dialled.driver/redis/sentinel.go:476-481(handleSwitchMaster, retry backoff vscloseCh): same shape and NOT guarded: ifcloseChis closed and the timer is ready too, the loop dials the master again afterClose(at most twice more,maxRetriesis 3). It also has a second defect with no select in it: after the loop,s.client = newClient(line 487) has noclosedcheck, so aClosethat lands whiledialMasterruns (it uses its own timeout, notcloseCh) leaves a live client installed on a closedSentinelClientthat nothing closes. A select priority alone would not fix that. driver/redis is in this module; I left it out of this PR for scope: the fix is aclosedcheck unders.mubefore the install, a different change in a different package. Read, not reproduced here; it is fixed in driver/redis: SentinelClient and ClusterClient no longer leak or panic when closed during a failover or a refresh #1141.driver/memcached/cluster.go:480(probe ticker vsctx.Done()): a probe after a done ctx runs withcontext.WithTimeout(ctx, ...), so it fails at once; no side effect.driver/redis/cluster.go:465(60 s refresh ticker vscloseCh): the select race itself has no side effect: afterClose,closeNodeshas setc.nodes = nil,anyNode()returns nil andrefreshTopologyreturns "no live nodes". A separate latent defect, read and not reproduced: ifDo(CLUSTER SLOTS)finished beforeCloseandCloserunscloseNodesbeforerefreshTopologytakesc.mu, the writec.nodes[addr] = n(cluster.go ~381, ~408) goes into a nil map and panics. Also fixed in driver/redis: SentinelClient and ClusterClient no longer leak or panic when closed during a failover or a refresh #1141 (the refresh and the redirect writer).internal/engine/std/engine.go:355(h2 stream poll vsctx.Done()): the step after the select iscloseH2Streams, a compare-and-swap that is the shutdown's own action; no harm if it runs after the budget.internal/adaptive/engine.go:629and:811(bind poll loops),internal/deferlinger/deferlinger.go:341,driver/postgres/conn.go:1273, 3067, 3141(30 s timeouts that fail the connection, withdoneChas the other case),internal/recvtheft(test hooks): no side effect after a ready timer that a ready second case should have prevented.So: one more site of the same shape with a real defect (
sentinel.go:477), whose fix is different, listed and not changed.Release note
None user-visible beyond: a driver dial on the io_uring engine whose ctx is done no longer makes a further registration attempt.