mirror of
https://github.com/luxfi/zap.git
synced 2026-10-30 15:04:16 +00:00
Stop took discovery down first. acceptLoop wakes only when the listener closes, so while discovery was stranded in a multicast write the listener stayed open, acceptLoop stayed parked in Accept, and the Wait at the end of Stop waited for it. One blocked write held the whole node. Closing the listener and the connections first means nothing later in Stop can pin a goroutine Wait is waiting for. That Wait is also bounded now. Two of the goroutines under it run code this package does not own: dispatchLoop calls a registered Handler inline, and a connection still shaking hands is not yet in the conns map, so Stop cannot close it and it sits on its own read deadline. Closing everything is what actually releases them and releases them at once; the bound is there so that neither a peer nor a handler decides when the process exits. Start now records the Discovery only once it is running, so Stop after a failed Start finds nothing to take down -- base calls Stop from its terminate hook whether or not Start returned an error. The QUIC path went further and left transClose set behind its own error return, so Stop closed the transport a second time. Both paths now share one startDiscovery, which also settles their disagreement about mDNS: TCP treated a discovery failure as best-effort and QUIC treated it as fatal, so a node on a pod overlay without multicast could serve over TCP and refused to start over QUIC. luxfi/mdns v0.2.1 carries the matching bound underneath. go.sum recorded the pre-rewrite zip hash for luxfi/pq v1.0.3, which fails verification against the checksum log on any cold cache. Corrected to the hash sum.golang.org publishes; the go.mod hash was already right. Co-authored-by: Hanzo Dev <[email protected]>
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zap
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// stopCeiling is what a caller of Stop is promised: mDNS gives up after a
|
|
// second, zap waits at most waitBound on its own goroutines, and the rest is
|
|
// slack for a loaded machine.
|
|
const stopCeiling = 2*time.Second + waitBound
|
|
|
|
// A node that never started has nothing to take down. Base calls Stop from its
|
|
// terminate hook whether or not Start returned an error, so this is the path
|
|
// taken every time a port is already held.
|
|
func TestStopWithoutStart(t *testing.T) {
|
|
n := NewNode(NodeConfig{NodeID: "never-started", Port: pickFreePortNode(t), NoDiscovery: true})
|
|
|
|
start := time.Now()
|
|
n.Stop()
|
|
if elapsed := time.Since(start); elapsed > waitBound {
|
|
t.Fatalf("Stop took %v on a node that never started, want immediate", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestStopAfterFailedStart(t *testing.T) {
|
|
held, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer held.Close()
|
|
|
|
n := NewNode(NodeConfig{
|
|
NodeID: "failed-start",
|
|
Address: held.Addr().String(),
|
|
})
|
|
if err := n.Start(); err == nil {
|
|
t.Fatal("Start succeeded on a held port")
|
|
}
|
|
|
|
start := time.Now()
|
|
n.Stop()
|
|
if elapsed := time.Since(start); elapsed > waitBound {
|
|
t.Fatalf("Stop took %v after a failed Start, want immediate", elapsed)
|
|
}
|
|
}
|
|
|
|
// dispatchLoop runs a registered Handler inline, and a connection still shaking
|
|
// hands is not in the conns map for Stop to close. Neither can be made to
|
|
// return, so Stop stops waiting on them.
|
|
func TestStopLeavesWedgedWork(t *testing.T) {
|
|
n := NewNode(NodeConfig{NodeID: "wedged", Port: pickFreePortNode(t), NoDiscovery: true})
|
|
n.wg.Add(1) // stands in for that work; nothing will ever call Done
|
|
|
|
start := time.Now()
|
|
n.Stop()
|
|
elapsed := time.Since(start)
|
|
|
|
if elapsed < waitBound {
|
|
t.Errorf("Stop returned after %v, inside the %v bound -- it did not wait at all", elapsed, waitBound)
|
|
}
|
|
if elapsed > waitBound+time.Second {
|
|
t.Errorf("Stop took %v, want under %v", elapsed, waitBound+time.Second)
|
|
}
|
|
}
|
|
|
|
// The whole teardown, discovery included, on a node that really started. On a
|
|
// host with an interface that takes no multicast this is the path that used to
|
|
// never return.
|
|
func TestStopBounded(t *testing.T) {
|
|
port := pickFreePortNode(t)
|
|
n := NewNode(NodeConfig{NodeID: "bounded", ServiceType: "_zapstop._tcp", Port: port})
|
|
if err := n.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
|
|
start := time.Now()
|
|
n.Stop()
|
|
if elapsed := time.Since(start); elapsed > stopCeiling {
|
|
t.Fatalf("Stop took %v, want under %v", elapsed, stopCeiling)
|
|
}
|
|
|
|
ln, err := net.Listen("tcp", n.listenAddr())
|
|
if err != nil {
|
|
t.Fatalf("port %d still held after Stop: %v", port, err)
|
|
}
|
|
ln.Close()
|
|
}
|