mirror of
https://github.com/soypat/lneto.git
synced 2026-09-09 00:09:08 +00:00
* fix(xnet): make blocking waits deadline-driven instead of iteration-capped
All six StackBlocking wait loops run 'for range maxIter' (1000). With a
non-sleeping backoff like BackoffFlagGosched, the sensible choice on
GOMAXPROCS=1 targets where sleeping starves the NIC poll, those 1000
iterations complete in ~20ms of wall time and silently replace the
caller's timeout: a dial with the default 2s timeout fails after ~21ms
with a spurious deadline error against any peer slower than that.
Observed dialing github.com from a single-core bare-metal (tamago)
target through go-net.
The deadline check inside every loop is the real guard, so it becomes the
loop condition itself and keeps the bounded lifetime visible on the for
statement:
for ok := true; ok; ok = s.checkDeadline(deadline) == nil {
DoDHCPv4 loses its separate deadline branch as a result. It used to check
only on iterations without a state change, which without the iteration
cap would let a peer that keeps feeding state transitions hold the loop
past any deadline; from the header the check runs every iteration.
The regression test drives a dial through 4000 no-progress wait
iterations on simulated time (<5% of the deadline elapsed) and requires
establishment once the handshake is finally serviced; on current main it
fails at exactly iteration 1000.
* go fix
---------
Co-authored-by: Derek den Haas <d.haas@directcode.com>
This commit is contained in:
@@ -1232,3 +1232,88 @@ func TestEphemeralPortSequence(t *testing.T) {
|
||||
t.Fatalf("post-cycle port %d below dynamic range", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStackGoTCPDialSurvivesManyWaitIterations checks the dial wait ends on its
|
||||
// deadline and not on an iteration count: the peer stays silent for several
|
||||
// times the former maxIter while simulated time advances by a fraction of the
|
||||
// timeout, and the dial must still establish once the handshake is serviced.
|
||||
func TestStackGoTCPDialSurvivesManyWaitIterations(t *testing.T) {
|
||||
const seed = 91011
|
||||
const MTU = ethernet.MaxMTU
|
||||
const tcptimeout = time.Second
|
||||
const quietIters = 4000 // well past the former iteration cap
|
||||
client, sv, _, svconn := newTCPStacks(t, seed, MTU)
|
||||
err := sv.ListenTCP4(svconn, 22)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tsched := ltesto.NewSched(t)
|
||||
tgoro := tsched.Goro()
|
||||
sg := client.StackBlocking(tgoro.Yield).StackGo(StackGoConfig{
|
||||
ListenerPoolConfig: TCPPoolConfig{
|
||||
QueueSize: 4,
|
||||
TxBufSize: MTU,
|
||||
RxBufSize: MTU,
|
||||
NewBackoff: func() lneto.BackoffStrategy {
|
||||
return backoffYield
|
||||
},
|
||||
},
|
||||
TCPDialTimeout: tcptimeout,
|
||||
TCPDialRetries: 1,
|
||||
})
|
||||
// Simulated clock: the quiet phase below advances less than 5% of the dial
|
||||
// timeout, so a timeout error there can only come from iteration counting.
|
||||
var now time.Duration
|
||||
sg.blk._nanotime = func() int64 { return int64(now) }
|
||||
|
||||
laddr := netip.AddrPortFrom(netip.AddrFrom4(client.Addr4()), 1234)
|
||||
raddr := netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), 22)
|
||||
go func() {
|
||||
_, err := sg.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM, laddr, raddr)
|
||||
tgoro.FinishWithErr(err)
|
||||
}()
|
||||
|
||||
// Quiet phase: no packets serviced, so the dialer only spins.
|
||||
for i := range quietIters {
|
||||
done, err := tsched.AwaitGoroYieldOrDone()
|
||||
if done {
|
||||
t.Fatalf("dial gave up during quiet phase after %d iterations: %v", i, err)
|
||||
}
|
||||
now += tcptimeout / 100000
|
||||
tsched.YieldToGoro()
|
||||
}
|
||||
|
||||
// Handshake phase: pump packets until the dial completes, bounded rounds so
|
||||
// a broken handshake fails loudly.
|
||||
var buf [ethernet.MaxMTU + ethernet.MaxOverheadSize]byte
|
||||
for range 64 {
|
||||
done, err := tsched.AwaitGoroYieldOrDone()
|
||||
if done {
|
||||
if err != nil {
|
||||
t.Fatalf("dial failed after handshake serviced: %v", err)
|
||||
}
|
||||
return // Established under deadline: test success.
|
||||
}
|
||||
n, err := client.EgressEthernet(buf[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n > 0 {
|
||||
if err := sv.IngressEthernet(buf[:n]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
n, err = sv.EgressEthernet(buf[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n > 0 {
|
||||
if err := client.IngressEthernet(buf[:n]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
now += tcptimeout / 100000
|
||||
tsched.YieldToGoro()
|
||||
}
|
||||
t.Fatal("dial did not establish within handshake rounds")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user