Skip to content

Tell callers when the snode pool could not be refreshed - #157

Open
mpretty-cyro wants to merge 1 commit into
session-foundation:devfrom
mpretty-cyro:fix/refresh-callback-contract
Open

Tell callers when the snode pool could not be refreshed#157
mpretty-cyro wants to merge 1 commit into
session-foundation:devfrom
mpretty-cyro:fix/refresh-callback-contract

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

The defect

refresh_if_needed had three ways to do nothing and say nothing — a suspended pool, no candidate
nodes, no fetcher. In each, on_refresh_complete was simply never invoked.

Every caller sets its own state up before calling, so "never" is not a delay, it is permanent:

caller cost of a dropped callback
Network::_resync_clock sets _current_clock_resync_id first, so every later clock resync short-circuits as "already in progress" and everything in _clock_resync_request_queue is stranded
OnionRequestRouter setup _finish_setup() never runs — the router never becomes usable
SessionRouterRouter setup same
Network::get_random_nodes the client's callback never fires
OnionRequestRouter path build every request queued for that category waits forever
SessionRouterRouter proxy selection the request's callback never fires

The 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_nodes re-enters itself from its callback, and
Loop::call runs inline when already on the loop thread, so that is genuine stack recursion; the path
builder rebuilds straight back into the same too-few-nodes branch.

SnodePool has no C API surface, so this is a C++-internal contract change — no wrapper or client
release train.

The judgement calls, which are the thing to review

Each caller needed a failure branch, and these are decisions rather than mechanics:

  • Both routers still finish setup. Not finishing leaves them permanently unusable; finishing with
    an empty pool just means the first request refreshes again.
  • _resync_clock routes into the existing _on_clock_resync_complete(), whose own comment
    already 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.
  • That function's total_requests parameter 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 that
    also won't happen.

Tests

New [network][refresh_callback_contract] case covering all three outcomes: cannot start, nothing
needed, suspended. Mutation-verified — restoring the silent return at the no-candidates bail fails
CHECK(called).

Worth noting the coverage that actually protects this refactor is the compiler: override failed both
test doubles the moment the signature changed.

Full suite green (135 cases). utils/format.sh verify clean.

Forward-port

client has the identical defect (still std::function<void()>, still the same three silent
returns). The patch conflicts there on its _loop->call_jq.call job-queue refactor.

`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
mpretty-cyro marked this pull request as ready for review September 11, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant