Tell callers when the snode pool could not be refreshed - #157
Open
mpretty-cyro wants to merge 1 commit into
Open
Tell callers when the snode pool could not be refreshed#157mpretty-cyro wants to merge 1 commit into
mpretty-cyro wants to merge 1 commit into
Conversation
`refresh_if_needed` had three ways to do nothing and say nothing: a suspended pool, no candidate nodes, and no fetcher. The callback was simply never invoked, and every caller sets its own state up before calling, so "never" is not a delay - it is permanent: - `_resync_clock` sets `_current_clock_resync_id` first, so a dropped callback makes every later clock resync short-circuit as "already in progress" and strands everything queued behind it. - both routers finish setup from the callback, so they never finish it. - `get_random_nodes` and the path builder retry from the callback, so their callers wait on something that will not arrive. The callback now takes whether a refresh actually happened, which is the smallest thing that lets a caller tell "the pool is fine" from "we could not find out". Nothing needing a refresh reports true - that is the answer the caller wanted, not a failure to get one. Each caller had to be given a failing branch, and two of them would have spun rather than hung if the callback had simply always been invoked: `get_random_nodes` re-enters itself, and the path builder rebuilds into the same too-few-nodes branch. `SnodePool` has no C API surface, so this is internal only.
mpretty-cyro
marked this pull request as ready for review
September 11, 2026 04:27
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.
The defect
refresh_if_neededhad three ways to do nothing and say nothing — a suspended pool, no candidatenodes, no fetcher. In each,
on_refresh_completewas simply never invoked.Every caller sets its own state up before calling, so "never" is not a delay, it is permanent:
Network::_resync_clock_current_clock_resync_idfirst, so every later clock resync short-circuits as "already in progress" and everything in_clock_resync_request_queueis strandedOnionRequestRoutersetup_finish_setup()never runs — the router never becomes usableSessionRouterRoutersetupNetwork::get_random_nodesOnionRequestRouterpath buildSessionRouterRouterproxy selectionThe change
The callback now takes whether a refresh actually happened. "Nothing needed refreshing" reports
true— that is the answer the caller asked for, not a failure to get one.The flag is doing real work rather than being decoration: simply always invoking the callback would
have turned two of these hangs into spins.
get_random_nodesre-enters itself from its callback, andLoop::callruns inline when already on the loop thread, so that is genuine stack recursion; the pathbuilder rebuilds straight back into the same too-few-nodes branch.
SnodePoolhas no C API surface, so this is a C++-internal contract change — no wrapper or clientrelease train.
The judgement calls, which are the thing to review
Each caller needed a failure branch, and these are decisions rather than mechanics:
an empty pool just means the first request refreshes again.
_resync_clockroutes into the existing_on_clock_resync_complete(), whose own commentalready says it runs "regardless of whether it was successful or not". It resets the in-progress id
and fails the queued requests, and leaves the existing network offset alone — the offset is only
written under
if (count > 0). An old offset beats none.total_requestsparameter was dead (commented out, unreferenced) and is dropped —calling it with a literal invited exactly the wrong reading of what it does.
get_swarm's deferred re-run returns an empty swarm rather than re-deferring against a refresh thatalso won't happen.
Tests
New
[network][refresh_callback_contract]case covering all three outcomes: cannot start, nothingneeded, suspended. Mutation-verified — restoring the silent
returnat the no-candidates bail failsCHECK(called).Worth noting the coverage that actually protects this refactor is the compiler:
overridefailed bothtest doubles the moment the signature changed.
Full suite green (135 cases).
utils/format.sh verifyclean.Forward-port
clienthas the identical defect (stillstd::function<void()>, still the same three silentreturns). The patch conflicts there on its
_loop->call→_jq.calljob-queue refactor.