Reported by an operator whose node sends little: nothing for two minutes, then a reconnect — losing the login, the filters, and any spot that arrived during the gap. The read had a 120 s deadline and ANY error ended the session, a timeout included. That reads a silence as a verdict on the link. It is not: a cluster on a dead band legitimately says nothing for minutes. Only a real error ends a session now. A genuinely dead peer is still caught, and by the mechanism meant for it — TCP keepalive probes an idle connection and its failure surfaces as an error on Read, not as a timeout. The dialer sets it explicitly rather than inheriting a default, since it is now what the design depends on. One trap the advice this came from did not mention: ReadString returns the bytes it HAD read along with the timeout. Discarding them would lose the first half of any spot line straddling a deadline, so the partial is carried over. Tested: a listener that goes silent past the deadline and then sends a spot — the spot arrives on the ORIGINAL connection, and the listener never accepts a second one. The tick is a var so that test runs in four seconds instead of sixty; a slow test is a test that gets skipped.
90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
package cluster
|
|
|
|
import (
|
|
"net"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A quiet cluster must not be treated as a broken one.
|
|
//
|
|
// Reported by an operator whose node sends little: nothing for two minutes and
|
|
// OpsLog reconnected — losing the login, the filters, and any spot that landed
|
|
// during the gap. The read deadline was being read as a verdict on the link
|
|
// rather than as a way to stay responsive to a stop request.
|
|
//
|
|
// The test holds the connection open, silent for longer than one deadline, then
|
|
// sends a spot. A session that survives receives it; one that reconnects does
|
|
// not, because this listener never accepts twice.
|
|
func TestSilenceDoesNotEndTheSession(t *testing.T) {
|
|
// Shorten the tick so the test exercises the real code path in seconds.
|
|
defer func(orig time.Duration) { idleTick = orig }(idleTick)
|
|
idleTick = time.Second
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
accepted := make(chan net.Conn, 2)
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
accepted <- c
|
|
}
|
|
}()
|
|
|
|
host, portStr, _ := net.SplitHostPort(ln.Addr().String())
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
spots := make(chan Spot, 4)
|
|
s := &session{
|
|
cfg: ServerConfig{Name: "quiet", Host: host, Port: port},
|
|
onSpot: func(sp Spot) { spots <- sp },
|
|
onLine: func(Line) {},
|
|
onStatus: func() {},
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
|
|
done := make(chan error, 1)
|
|
go func() { _, err := s.runOnce(); done <- err }()
|
|
|
|
conn := <-accepted
|
|
defer conn.Close()
|
|
|
|
// Silent for longer than one deadline: the loop must ride through it.
|
|
time.Sleep(3 * idleTick)
|
|
|
|
select {
|
|
case c := <-accepted:
|
|
c.Close()
|
|
t.Fatal("the session reconnected during the silence")
|
|
case err := <-done:
|
|
t.Fatalf("the session ended during the silence: %v", err)
|
|
default:
|
|
}
|
|
|
|
// Now a spot: it proves the ORIGINAL connection is still the live one.
|
|
if _, err := conn.Write([]byte("DX de F4BPO: 14074.0 OY1CT FT8 1234Z\r\n")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case sp := <-spots:
|
|
if sp.DXCall != "OY1CT" {
|
|
t.Errorf("spot from the wrong station: %+v", sp)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the spot sent after the silence never arrived")
|
|
}
|
|
|
|
close(s.stopCh)
|
|
<-done
|
|
}
|