Files
olcrtc/internal/e2e/deadlink_test.go
neuronori 13f77b1da7 test(e2e): prove dead-link detection window via blackhole carrier
add a silent dead-link mode to the memory carrier (link stays up,
frames vanish) and a flag-gated TestDeadLinkDetectionWindow that
measures how long the datachannel client takes to notice a dead link
and reconnect. on the conservative liveness window it reconnects in
~58s; with the buggy 120s smux keepalive it fails to detect within 75s.

also drop dead restartControlKCP (both callers moved to
restartControlKCPWithHeader)
2026-06-24 14:54:19 +00:00

121 lines
4.0 KiB
Go

package e2e
import (
"context"
"flag"
"sync/atomic"
"testing"
"time"
"github.com/openlibrecommunity/olcrtc/internal/client"
"github.com/openlibrecommunity/olcrtc/internal/control"
"github.com/openlibrecommunity/olcrtc/internal/server"
)
// deadLinkEnabled gates TestDeadLinkDetectionWindow. It runs a real
// in-process client/server tunnel over the memory carrier, then silently
// black-holes the carrier (link stays "up", frames vanish) and measures how
// long the client takes to notice the dead link and trigger a reconnect.
//
// This is the exact failure mode behind the jitsi/datachannel regression:
// the peer leaves but the WebRTC PC has not torn down, so the only signal is
// the control-stream liveness timeout. The bad commit widened that timeout to
// 120s globally; the fix scopes the long window to ControlPlane transports so
// datachannel detects a dead link on the conservative ~30s smux keepalive.
var deadLinkEnabled = flag.Bool( //nolint:gochecknoglobals // package-level state intentional
"olcrtc.deadlink",
false,
"run TestDeadLinkDetectionWindow (measures dead-link detection latency)",
)
// TestDeadLinkDetectionWindow proves the datachannel dead-link detection
// window returned to the conservative band after the fix. It fails if the
// client does not detect the silently-dead carrier and reconnect within the
// expected window (well under the buggy 120s, comfortably above the
// conservative 30s keepalive timeout).
func TestDeadLinkDetectionWindow(t *testing.T) {
if !*deadLinkEnabled {
t.Skip("dead-link test disabled; pass -olcrtc.deadlink to enable")
}
const (
transportName = transportData // datachannel: no isolated control plane
// The conservative smux keepalive timeout is 30s (interval 10s). Allow
// generous headroom for scheduling + a probe cycle, but stay well below
// the buggy 120s so the bad commit fails this bound.
detectionBudget = 75 * time.Second
setupBudget = 30 * time.Second
)
carrierName, room := registerMemoryCarrier(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
socksAddr := freeLocalAddr(ctx, t)
// Count reconnects observed by the client's health hook. A dead-link
// detection manifests as a reconnect (control loop ends -> handleReconnect).
var reconnects atomic.Uint64
detected := make(chan struct{}, 1)
onHealth := func(st control.Status) {
if st.Reconnects > 0 || st.UnhealthyEvents > 0 {
if reconnects.Swap(st.Reconnects) == 0 {
select {
case detected <- struct{}{}:
default:
}
}
}
}
serverErr := make(chan error, 1)
go func() {
serverErr <- server.Run(ctx, server.Config{
Transport: transportName,
Carrier: carrierName,
RoomURL: testRoom,
KeyHex: testKeyHex,
DNSServer: localDNSServer,
})
}()
room.waitConnected(t, 1)
ready := make(chan struct{})
clientErr := make(chan error, 1)
go func() {
clientErr <- client.RunWithReady(ctx, client.Config{
Transport: transportName,
Carrier: carrierName,
RoomURL: testRoom,
KeyHex: testKeyHex,
DeviceID: testClientDeviceID,
LocalAddr: socksAddr,
DNSServer: localDNSServer,
OnHealth: onHealth,
}, func() { close(ready) })
}()
waitForReadyWithin(t, ready, setupBudget)
// Let the control stream settle into a healthy steady state first.
time.Sleep(2 * time.Second)
// Silently kill the carrier: link stays up, frames are dropped. Only the
// control-stream liveness timeout can detect this.
t.Logf("[deadlink] enabling blackhole at %s", time.Now().Format("15:04:05"))
start := time.Now()
room.enableBlackhole()
select {
case <-detected:
elapsed := time.Since(start)
t.Logf("[deadlink] dead link detected + reconnect in %s", elapsed.Round(time.Millisecond))
if elapsed > detectionBudget {
t.Fatalf("dead-link detection took %s, want <= %s (regression: liveness window too wide)",
elapsed.Round(time.Second), detectionBudget)
}
case <-time.After(detectionBudget):
t.Fatalf("dead link NOT detected within %s (regression: client treats dead carrier as alive)",
detectionBudget)
}
}