Repository navigation
[go] Fix notifier hanging after a listen or unlisten times out - #1559
Merged
brandur merged 1 commit intoOct 29, 2026
Merged
Conversation
When the notifier is waiting for notifications, `Listen` and `unlisten` hand their `LISTEN` or `UNLISTEN` to the run loop and wait up to 5 seconds for the result. The result channel was unbuffered, so if the operation finished after the caller had given up, the run loop blocked forever sending a result nothing would receive. The notifier then never waited for notifications again, and its `Stop` never returned, so stopping the client never completed either, and a `Client.Start` that failed on the timeout never returned. Buffer the channel so the run loop's send always goes through. Signed-off-by: Yaomeng1749 <176285760+Yaomeng1749@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Contributor
|
Thanks! |
brandur
approved these changes
Oct 29, 2026
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.
When the notifier is waiting for notifications,
Listen(for a topic's first subscription) andunlisten(for its last) hand theirLISTENorUNLISTENto the run loop throughsendInterruptAndReceiveResult, which gives up after 5 seconds withtimed out waiting for interrupt operation. The channel the result comes back on was unbuffered, so when the operation finished after the caller had given up, the run loop blocked forever sending a result that nothing would receive. The notifier never waited for notifications again, and itsStopnever returned.All it takes is for the run loop to need more than 5 seconds to get to the operation and finish it, for example on a slow or unresponsive connection, where
LISTENandUNLISTENeach get the 10-secondlistenerTimeout. These calls come from producers and the elector as a client starts and stops, so once the notifier was stuck:Client.Stopnever completed, since stopping the client stops the notifier.Client.Startalso stops its services before returning, so when a producer's or the elector'sListenwas the one that timed out,Client.Startnever returned.Queues().Removeand adding one back, its producers also stopped hearing about job cancellations and queue changes, since with a listener they don't poll for them.I think this is what the logs in #644 show. The producer's
river_insertsubscription is removed exactly 5 seconds after it's added, the notifier logsListening on topicfor it 4 ms later, the elector's unlisten then times out as well, and the log ends without the notifier'sRun loop stopped. The shutdown there looks likeClient.Startcleaning up after the producer's failedListenrather than a regular stop: there's noRiver client started, noClient: Stopping producers, and the producer never logsRun loop started. That would explain whyStartnever returned. With this change, it would return the timeout error instead.The fix gives the channel a buffer of one, as #1205 did for
leaderResignedChan, so the run loop's send always goes through, and the result is dropped if the caller has given up. A longer timeout would only make this less likely, since it also has to cover the time before the run loop gets to the operation. I didn't add logging because the caller already returns or logs the timeout error. This also doesn't change what a timeout leaves behind. For example, a timed-outunlistenstill returns before removing its subscription, as before.TestNotifier_Start/InterruptTimeoutusessynctestand a listener whoseListentakes 6 seconds. It checks thatListenreturns the timeout error, and that once the operation finishes, the run loop goes back to waiting for notifications, runs anotherListenthrough an interrupt, and stops. Without the fix, the run loop is still stuck sending the first result, so the test fails and synctest reports the deadlock.I checked the Java, JS, Ruby, and Rust ports, and they aren't affected. They
LISTENon a fixed set of topics when they connect, so none of them hands subscription changes to a running wait loop.Locally against Postgres 16, the new subtest passes with
-race -count=20on Go 1.26 and 1.27, andmake test,make test/race TEST_DATABASE=postgres, andmake lintpass.#1361 also adds a
CHANGELOG.mdentry under Unreleased, so whichever of the two lands second will need a rebase there, which I'm happy to do. It changesnotifier.goand its tests too, but notsendInterruptAndReceiveResult, and apart fromCHANGELOG.mdit merges cleanly with this.