A machine that wakes or moves says so, instead of waiting to be told

A suspended laptop's connection is dead the moment it wakes, and the socket
looks perfectly healthy from inside the process — no error, no close, because
nothing has tried to send anything. Heartbeats find out twenty or thirty
seconds later. For that time the node believes it is in a mesh it has left,
which is the one state this design says must never be indistinguishable from
being connected. The machine knew immediately.

So being roused ends the current attempt rather than only shortening the wait
after it: shortening the wait would do nothing at all, because the process is
not waiting — it is sitting inside a connection that will not return.

A signal, because nothing may listen on a node (novox/hq ADR 0004). A socket
for this would be a control surface on every machine, reachable by anything
that can reach the machine, in exchange for saving twenty seconds — and the
whole security argument rests on there not being one.

Two rouses in the same instant are one: a machine suspending and resuming
repeatedly must not build a backlog of reconnections to work through. And the
backoff is not reset by being roused — that says the machine changed, not that
whatever was refusing the connection has stopped, and a laptop woken on a
network with no route would otherwise retry at full speed for as long as
somebody keeps opening the lid.

The dispatcher acts on the events that change where packets go and not on
`down`: the link is already gone there, reconnecting will fail, and the backoff
exists for exactly that.
This commit is contained in:
2026-08-31 10:21:26 +02:00
parent 5bc0006e83
commit 8fcfa88fe0
8 changed files with 302 additions and 3 deletions
+38 -2
View File
@@ -602,13 +602,49 @@ func runLink(ctx context.Context, opts options) error {
// (novox/hq ADR 0004).
go holdTheMachine(ctx, opts, mine, say)
return link.Hold(ctx, link.Membership{
return link.HoldRoused(ctx, link.Membership{
Node: mine.Node,
Broker: mine.Membership.Broker,
Fingerprint: mine.Membership.Fingerprint,
Password: mine.Membership.Password,
Signer: mine.Membership.Signer,
}, apply, say, opts.timeout)
}, apply, say, opts.timeout, rousedBySignal(ctx))
}
// rousedBySignal is the machine telling this process that its link is probably stale.
//
// **A signal, because nothing may listen on a node** (novox/hq ADR 0004). A socket for this would
// be a control surface on every machine, reachable by anything that can reach the machine, in
// exchange for saving twenty seconds — and the whole security argument rests on there not being
// one. A signal is delivered by the service manager to a process it already supervises.
//
// SIGHUP, because that is the signal a long-running program conventionally reads as *look again*,
// and nothing here is being reloaded from a file that a different signal would suit better.
//
// Dropped rather than queued when one arrives while another is unread: two wakes in the same
// instant are one wake, and a machine that suspends and resumes repeatedly must not build a
// backlog of reconnections to work through.
func rousedBySignal(ctx context.Context) link.Roused {
woken := make(chan os.Signal, 1)
signal.Notify(woken, syscall.SIGHUP)
out := make(chan struct{}, 1)
go func() {
defer signal.Stop(woken)
for {
select {
case <-ctx.Done():
return
case <-woken:
select {
case out <- struct{}{}:
default:
// One is already waiting to be read. Two wakes in the same instant are one.
}
}
}
}()
return out
}
// ReconcileEvery is how often a node re-applies what it was last told.