Skip to content

A relay that never answers the dial no longer holds req and fetch - #20

Merged
sepehr-safari merged 1 commit into
mainfrom
a-relay-that-never-answers-the-dial
Sep 23, 2026
Merged

sepehr-safari merged 1 commit into
mainfrom
a-relay-that-never-answers-the-dial

Conversation

@sepehr-safari

Copy link
Copy Markdown
Contributor

Refs #19. This covers req and fetch; publish dials the same way and moves onto the same path in the change that reworks what it returns.

The relays were dialled one after another with no deadline, and --timeout only started counting once every dial had returned. A relay that accepted the connection and never answered the websocket upgrade held the run forever, and the relays listed after it were never reached.

What changes

  • Every relay is dialled at once. A timer runs beside the dials, and whatever is still dialling when it fires is cancelled, named on stderr as no answer within <n> ms, and left out. The run carries on with the relays that answered.
  • The bound is five seconds, or --timeout if that is shorter. --timeout still covers the wait for answers once the relays are reached.
  • The nostr pin moves to 0.14.4, where a cancelled dial stops at once and frees what it allocated. Before that, a cancel could be swallowed and the dial ran on anyway.
  • The name lookup is the one step a cancel cannot cut short, because it is a plain libc call. The help for req and fetch says what the bound is.

Checked against real relays

Relays Before After
a local relay that never answers, then nos.lol no output, never exits 5.2 s, one event, the silent relay named
relay.nostr.band (stalling its TLS handshake today), then nos.lol 60 s 5.2 s, one event
the silent local relay, with --timeout 2000 never exits 2.2 s

Tests

The tests dial a real relay over loopback, through the same dial a run uses:

  • a silent relay times out, a closed port fails, and a live relay connects, all at once checks each outcome and that the whole thing takes the deadline, not the slowest relay. With the timer made to fire late, it fails.
  • a relay that never answers the dial costs the run its deadline, not forever puts the silent relay first, ahead of a live one, and requires the live relay to answer and the run to finish.

Dialling tests use a leak-checked allocator that captures no stack traces. On macOS a stack capture can swallow a pending cancel, and the test would hang instead of failing. The test relay serves one connection and returns for the same reason. The suite passed 20 runs of 20 with no hang.

All 70 tests pass.

The relays were dialled one after another with no deadline, and --timeout only started counting once every dial had returned. A relay that accepted the connection and never answered the websocket upgrade held the run forever, and the relays listed after it were never reached.

Every relay is now dialled at once, under a bound of five seconds or --timeout if that is shorter. A relay still dialling at the bound is cancelled, named on stderr, and left out, and the run carries on with the ones that answered. This needs nostr 0.14.4, where a cancelled dial stops at once and frees what it allocated, so the pin moves to it.

Tests dial a real relay on loopback: one that never answers, a closed port and a live one, all at once and inside the deadline.

Refs #19.
@sepehr-safari
sepehr-safari merged commit 34a8e6d into main Sep 23, 2026
2 checks passed
@sepehr-safari
sepehr-safari deleted the a-relay-that-never-answers-the-dial branch September 23, 2026 09:57
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