Skip to content

Commit c8c6067

Browse files
h4x3rotabclaude
andcommitted
fix(consul-postgres-ha): mesh-conn PacketConn shim must delegate deadlines
Discovered while verifying terraform's beta.3 env-update path against the live cluster on 2026-05-04: when terraform recreates a CVM, the peers' QUIC links to it die, but the *redial* path can hang. Specifically: dialICE returns a Connected ice.Conn, dialAndPump enters quic.Dial, ICE later goes Failed (peer went away again, hairpin lost, etc.). quic.Dial's context times out and quic-go calls SetReadDeadline(past) to interrupt the blocked ReadFrom in our iceConnPacketConn shim. The shim was returning nil from SetReadDeadline, so the call had no effect on the underlying ice.Conn.Read, and the goroutine hung forever. The surrounding runPeerLink retry loop never got to retry, leaving the peer slot permanently dead until the entire mesh-conn process was restarted. Fix: delegate SetDeadline / SetReadDeadline / SetWriteDeadline to the underlying conn (pion/ice.Conn implements net.Conn deadlines properly). Same fix applied to the stage4/quic-on-ice smoke test so future debugging stays trustworthy. Adds a regression test using net.Pipe (which honors deadlines) that asserts ReadFrom returns a Timeout-flagged net.Error within ~50ms of SetReadDeadline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f912d7 commit c8c6067

3 files changed

Lines changed: 62 additions & 10 deletions

File tree

consul-postgres-ha/stage4/mesh-conn/main.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -697,11 +697,20 @@ func (p *iceConnPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
697697
return p.conn.Write(b)
698698
}
699699

700-
func (p *iceConnPacketConn) Close() error { return p.conn.Close() }
701-
func (p *iceConnPacketConn) LocalAddr() net.Addr { return p.conn.LocalAddr() }
702-
func (p *iceConnPacketConn) SetDeadline(_ time.Time) error { return nil }
703-
func (p *iceConnPacketConn) SetReadDeadline(_ time.Time) error { return nil }
704-
func (p *iceConnPacketConn) SetWriteDeadline(_ time.Time) error { return nil }
700+
func (p *iceConnPacketConn) Close() error { return p.conn.Close() }
701+
func (p *iceConnPacketConn) LocalAddr() net.Addr { return p.conn.LocalAddr() }
702+
703+
// Deadline methods delegate to ice.Conn (via the embedded net.Conn on
704+
// countingConn) instead of being a no-op. quic-go relies on
705+
// SetReadDeadline to interrupt a blocked ReadFrom when its context
706+
// cancels — without this delegation, a quic.Dial whose context times
707+
// out (e.g. because ICE went Failed mid-handshake) would hang forever
708+
// in our shim, and the surrounding runPeerLink retry loop never gets
709+
// to retry. Pion's ice.Conn implements the deadline methods, so this
710+
// is the natural place to wire them through.
711+
func (p *iceConnPacketConn) SetDeadline(t time.Time) error { return p.conn.SetDeadline(t) }
712+
func (p *iceConnPacketConn) SetReadDeadline(t time.Time) error { return p.conn.SetReadDeadline(t) }
713+
func (p *iceConnPacketConn) SetWriteDeadline(t time.Time) error { return p.conn.SetWriteDeadline(t) }
705714

706715
// reportLinkStats logs a periodic summary per peer link. Once a minute,
707716
// and only when bytes actually moved since the last tick, so an idle

consul-postgres-ha/stage4/mesh-conn/validate_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"net"
45
"strings"
56
"testing"
7+
"time"
68
)
79

810
func TestValidatePeers_OK(t *testing.T) {
@@ -129,3 +131,41 @@ func TestValidatePeers_DigestDiffersWithDifferentPorts(t *testing.T) {
129131
t.Fatalf("digest collides for different ports")
130132
}
131133
}
134+
135+
// iceConnPacketConn must delegate deadline methods to the underlying
136+
// conn so quic-go can interrupt blocked reads on context cancel.
137+
// Returning nil from these methods (the previous behavior) leaves
138+
// quic.Dial hung when ICE goes Failed mid-handshake — the surrounding
139+
// runPeerLink retry loop then never gets to retry. Verified once at
140+
// 2026-05-04 against the live cluster; this test pins the behavior so
141+
// a future refactor doesn't regress.
142+
func TestIceConnPacketConn_DeadlinesPropagate(t *testing.T) {
143+
a, b := net.Pipe()
144+
defer a.Close()
145+
defer b.Close()
146+
147+
pkt := &iceConnPacketConn{conn: newCountingConn(a, "test")}
148+
149+
deadline := time.Now().Add(50 * time.Millisecond)
150+
if err := pkt.SetReadDeadline(deadline); err != nil {
151+
t.Fatalf("SetReadDeadline: %v", err)
152+
}
153+
154+
buf := make([]byte, 100)
155+
start := time.Now()
156+
_, _, err := pkt.ReadFrom(buf)
157+
elapsed := time.Since(start)
158+
159+
if err == nil {
160+
t.Fatal("ReadFrom returned nil error past the deadline")
161+
}
162+
netErr, ok := err.(net.Error)
163+
if !ok || !netErr.Timeout() {
164+
t.Fatalf("expected timeout net.Error, got %v (%T)", err, err)
165+
}
166+
// Generous bounds: net.Pipe's deadline implementation is precise
167+
// enough that 40-300ms covers test-VM jitter without flakes.
168+
if elapsed < 40*time.Millisecond || elapsed > 300*time.Millisecond {
169+
t.Errorf("ReadFrom returned in %v, expected ~50ms", elapsed)
170+
}
171+
}

consul-postgres-ha/stage4/quic-on-ice/main.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,14 @@ func (p *iceConnPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
100100
return p.conn.Write(b)
101101
}
102102

103-
func (p *iceConnPacketConn) Close() error { return p.conn.Close() }
104-
func (p *iceConnPacketConn) LocalAddr() net.Addr { return p.conn.LocalAddr() }
105-
func (p *iceConnPacketConn) SetDeadline(t time.Time) error { return nil }
106-
func (p *iceConnPacketConn) SetReadDeadline(t time.Time) error { return nil }
107-
func (p *iceConnPacketConn) SetWriteDeadline(t time.Time) error { return nil }
103+
func (p *iceConnPacketConn) Close() error { return p.conn.Close() }
104+
func (p *iceConnPacketConn) LocalAddr() net.Addr { return p.conn.LocalAddr() }
105+
106+
// Deadlines delegate to the ICE conn so quic-go can interrupt a
107+
// blocked ReadFrom on context cancel; mirrors mesh-conn/main.go.
108+
func (p *iceConnPacketConn) SetDeadline(t time.Time) error { return p.conn.SetDeadline(t) }
109+
func (p *iceConnPacketConn) SetReadDeadline(t time.Time) error { return p.conn.SetReadDeadline(t) }
110+
func (p *iceConnPacketConn) SetWriteDeadline(t time.Time) error { return p.conn.SetWriteDeadline(t) }
108111

109112
// =============================================================================
110113
// QUIC client / server over the PacketConn

0 commit comments

Comments
 (0)