Skip to content

Seed the address pool when an outbound connection finds no address. - #924

Open
echennells wants to merge 1 commit into
libbitcoin:masterfrom
echennells:fix/reseed-exhausted-address-pool
Open

echennells wants to merge 1 commit into
libbitcoin:masterfrom
echennells:fix/reseed-exhausted-address-pool

Conversation

@echennells

@echennells echennells commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

On an empty address pool the outbound session defers and retries, relying on peer gossip to refill it. With no channels nothing refills it, and as seeding runs only at startup, outbound connections stall until a manual or inbound peer connects. #942 keeps an outage from emptying the pool; this is the fallback when it empties anyway.

The outbound session now also seeds when it finds no address. The network retains its seed session, as it does the manual session; session_seed::seeding() is true from seed connection start until stop_seed, once all have stopped, so one session runs at a time by completion rather than timing. The startup session is retained the same way.

A new session starts only if the network is open, seeds are configured, the pool is below its minimum (as address_not_found also covers a pool with nothing usable for the slot), the retained session is not seeding, and none completed within 10 seeding timeouts.

@echennells
echennells requested a review from evoskuil September 28, 2026 20:58
@echennells
echennells force-pushed the fix/reseed-exhausted-address-pool branch from 3507963 to 1a3a033 Compare September 29, 2026 15:45
@evoskuil

Copy link
Copy Markdown
Member

Looks promising, we need this fix.

But first let's figure out why a network drop doesn't preserve addresses. That's an error code filtering issue. It's not only timeouts that should get returned to the pool, it's any failure that's not attributable to the address itself.

Comment thread src/net.cpp Outdated
BC_ASSERT(stranded());

// A seed session ends within the seeding timeout, so this also precludes
// concurrent sessions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too fragile for async programming. The seeding session can run forever, despite having a timeout, as closure is dependent upon the ASIO message queue, which can become backlogged. This would start up new sessions as previous ones are trying to stop, and that could lead to an unguarded backlog that would eventually bring down the process.

What you are after is idempotency and a definitive state to base that on: running or not running. That requires a seed session completion handler to bounce back to the calling strand and update a bool (e.g. seeding_), which must be checked (idempotent) and updated by the start.

Comment thread src/net.cpp Outdated

// The session logs its own result, and a failure is retried on demand.
seeded_ = now;
attach_seed_session()->start([](const code&) NOEXCEPT {});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the no-op handler being passed is the completion event that you need.

@echennells

Copy link
Copy Markdown
Contributor Author

Thanks. Two parts.

Why a network drop loses addresses: #942. An outage fails connects with ENETUNREACH, EADDRNOTAVAIL or, with the router gone, EHOSTUNREACH, and all of these mapped to connect_failed. A failed connect also returns no socket, so reclaim never ran for it. That PR splits those codes and restores the address in handle_one. With it the pool survives an outage, so this PR is now only the fallback for a pool that is actually empty.

Seeding guard: the timeout is gone. As you suggested, the state is contained in the seed session: session_seed::seeding() is true from seed connection start until stop_seed. The start handler fires on sufficiency, which can precede the last seed stopping, so stop_seed is the definitive completion. The network retains the seed session, as it does the manual session, and skips seeding without seeds, which duplicates a check in session_seed.

Would you rather the seed session reseed itself, keeping all of this within it?

@evoskuil

evoskuil commented Oct 4, 2026

Copy link
Copy Markdown
Member

Retaining the seed session and gating on seeding() is clean: it's strand-protected, start is synchronous so there's no window before seeding_ is set, and both close and the bypass paths release properly. The issue is the trigger.

  1. address_not_found means "nothing in the pool suits this slot", not "the pool is empty". take() also returns it when every pooled address is filtered out for this slot: wrong family for the slot's bind, a named address without a proxy, or reserved.

    • If the count is at or above the minimum, every slot creates a seed session on every connect_timeout, and each one logs "Bypassed seeding because of sufficient..." at LOGN.
    • If the count is below the minimum but nothing in the pool is usable, the node reseeds continuously while seeds are reachable.

    Suggest adding address_count() >= network_settings().outbound.minimum_address_count() to the do_seed guard. That avoids the session churn and the bypass logging. A backoff after a completed seed (e.g. none within some multiple of connect_timeout) would keep this from hammering seeds when the pool can't be made usable.

  2. Nit (session_seed.hpp). The seeding() doc should just state what the member does, e.g. /// Seed connections are running (call from network strand).

On having the seed session reseed itself: no, keep the trigger in outbound. Outbound is the only place that observes a failed take(). A self-reseeding session would have to poll the pool for the node's lifetime, and polling the count is a poor signal: addresses are out of the pool while connecting or connected, so a healthy node with a small pool would dip below the minimum and reseed anyway. It would also turn a one-shot session into a persistent one with its own timer and stop handling. Keeping the state in the seed session, as you have it, is the right split.

@echennells
echennells force-pushed the fix/reseed-exhausted-address-pool branch from 40deacf to ce33a1c Compare October 6, 2026 17:06
@echennells echennells changed the title Seed the address pool when an outbound connection finds it empty. Seed the address pool when an outbound connection finds no address. Oct 6, 2026
@echennells

Copy link
Copy Markdown
Contributor Author

Thanks, changes:

  1. do_seed now skips when address_count() >= outbound.minimum_address_count(), so a pool that is only unusable for a slot no longer creates seed sessions or logs the bypass.

  2. The seed session records when its connections complete (completed(), with seeding_ on the strand), and do_seed skips within 10 seeding timeouts of that (5 minutes by default). I used seeding_timeout because connect_timeout is re-randomized per call, and at 12 connect timeouts an unusable pool still reseeded about once a minute. A bypassed startup seeding leaves completed() zero, so the first reseed isn't delayed.

  3. seeding() doc as suggested.

Tests start the network, so the startup session is retained: a sufficient count or no seeds give no reseed; a running or just-completed session, startup or reseeded, blocks the next; otherwise each call reseeds.

@echennells
echennells requested a review from evoskuil October 6, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants