test: stop CI flakes in socket, daemon election, web reconnect, and QUIC tests
- commit
- 0b823c50bcbc2728b9de7e7bf9696c7c663caaa8
- author
- brice · · 2026-09-23 21:37 UTC
- parent
- e77fed7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +26 -4
M internal/adapters/ipc/mux_test.go +10 -3
| @@ -259,7 +259,14 @@ func createStaleMuxSocket(t *testing.T, path string) os.FileInfo { | |||
| 259 | 259 | // recovered race-safely and the carriage binds and serves. | |
| 260 | 260 | func TestListenMuxStaleSocketRecovered(t *testing.T) { | |
| 261 | 261 | path := muxTestPath(t) | |
| 262 | - | stale := createStaleMuxSocket(t, path) | |
| 262 | + | createStaleMuxSocket(t, path) | |
| 263 | + | // Mark the stale file: a filesystem such as ext4 may hand the freed inode | |
| 264 | + | // number straight to the recovered socket, so inode identity alone cannot | |
| 265 | + | // prove the file was replaced. | |
| 266 | + | marked := time.Unix(1, 0) | |
| 267 | + | if err := os.Chtimes(path, marked, marked); err != nil { | |
| 268 | + | t.Fatalf("mark stale socket: %v", err) | |
| 269 | + | } | |
| 263 | 270 | ||
| 264 | 271 | mustListenMux(t, path) | |
| 265 | 272 | ||
| @@ -267,8 +274,8 @@ func TestListenMuxStaleSocketRecovered(t *testing.T) { | |||
| 267 | 274 | if err != nil { | |
| 268 | 275 | t.Fatalf("stat recovered socket: %v", err) | |
| 269 | 276 | } | |
| 270 | - | if os.SameFile(stale, recovered) { | |
| 271 | - | t.Fatal("stale socket inode was not replaced by the recovered listener") | |
| 277 | + | if recovered.ModTime().Equal(marked) { | |
| 278 | + | t.Fatal("stale socket was not replaced by the recovered listener") | |
| 272 | 279 | } | |
| 273 | 280 | ||
| 274 | 281 | // The recovered carriage serves a same-user connection. | |
| @@ -259,7 +259,14 @@ func createStaleMuxSocket(t *testing.T, path string) os.FileInfo { | |||||
| 259 | // recovered race-safely and the carriage binds and serves. | 259 | // recovered race-safely and the carriage binds and serves. | ||
| 260 | func TestListenMuxStaleSocketRecovered(t *testing.T) { | 260 | func TestListenMuxStaleSocketRecovered(t *testing.T) { | ||
| 261 | path := muxTestPath(t) | 261 | path := muxTestPath(t) | ||
| 262 | - | stale := createStaleMuxSocket(t, path) | 262 | + | createStaleMuxSocket(t, path) |
| 263 | + | // Mark the stale file: a filesystem such as ext4 may hand the freed inode | |||
| 264 | + | // number straight to the recovered socket, so inode identity alone cannot | |||
| 265 | + | // prove the file was replaced. | |||
| 266 | + | marked := time.Unix(1, 0) | |||
| 267 | + | if err := os.Chtimes(path, marked, marked); err != nil { | |||
| 268 | + | t.Fatalf("mark stale socket: %v", err) | |||
| 269 | + | } | |||
| 263 | 270 | ||||
| 264 | mustListenMux(t, path) | 271 | mustListenMux(t, path) | ||
| 265 | 272 | ||||
| @@ -267,8 +274,8 @@ func TestListenMuxStaleSocketRecovered(t *testing.T) { | |||||
| 267 | if err != nil { | 274 | if err != nil { | ||
| 268 | t.Fatalf("stat recovered socket: %v", err) | 275 | t.Fatalf("stat recovered socket: %v", err) | ||
| 269 | } | 276 | } | ||
| 270 | - | if os.SameFile(stale, recovered) { | 277 | + | if recovered.ModTime().Equal(marked) { |
| 271 | - | t.Fatal("stale socket inode was not replaced by the recovered listener") | 278 | + | t.Fatal("stale socket was not replaced by the recovered listener") |
| 272 | } | 279 | } | ||
| 273 | 280 | ||||
| 274 | // The recovered carriage serves a same-user connection. | 281 | // The recovered carriage serves a same-user connection. | ||
M internal/adapters/quicnettest/proxy_test.go +6 -0
| @@ -54,6 +54,12 @@ func serveUDP(t *testing.T, echo bool) (*net.UDPConn, <-chan datagram) { | |||
| 54 | 54 | t.Fatalf("listen udp server: %v", err) | |
| 55 | 55 | } | |
| 56 | 56 | t.Cleanup(func() { _ = conn.Close() }) | |
| 57 | + | // Flows burst hundreds of MTU-sized datagrams. The default receive buffer | |
| 58 | + | // (about 200 KiB) holds too few of them when a loaded machine delays the | |
| 59 | + | // reader, and the kernel then drops datagrams the test is still counting. | |
| 60 | + | if err := conn.SetReadBuffer(4 << 20); err != nil { | |
| 61 | + | t.Fatalf("size udp server buffer: %v", err) | |
| 62 | + | } | |
| 57 | 63 | packets := make(chan datagram, 4096) | |
| 58 | 64 | go func() { | |
| 59 | 65 | buf := make([]byte, 2048) | |
| @@ -54,6 +54,12 @@ func serveUDP(t *testing.T, echo bool) (*net.UDPConn, <-chan datagram) { | |||||
| 54 | t.Fatalf("listen udp server: %v", err) | 54 | t.Fatalf("listen udp server: %v", err) | ||
| 55 | } | 55 | } | ||
| 56 | t.Cleanup(func() { _ = conn.Close() }) | 56 | t.Cleanup(func() { _ = conn.Close() }) | ||
| 57 | + | // Flows burst hundreds of MTU-sized datagrams. The default receive buffer | |||
| 58 | + | // (about 200 KiB) holds too few of them when a loaded machine delays the | |||
| 59 | + | // reader, and the kernel then drops datagrams the test is still counting. | |||
| 60 | + | if err := conn.SetReadBuffer(4 << 20); err != nil { | |||
| 61 | + | t.Fatalf("size udp server buffer: %v", err) | |||
| 62 | + | } | |||
| 57 | packets := make(chan datagram, 4096) | 63 | packets := make(chan datagram, 4096) | ||
| 58 | go func() { | 64 | go func() { | ||
| 59 | buf := make([]byte, 2048) | 65 | buf := make([]byte, 2048) | ||
M internal/app/broker_daemon_start_test.go +7 -0
| @@ -429,6 +429,13 @@ func TestDaemonStartConcurrentRequestsShareOneElection(t *testing.T) { | |||
| 429 | 429 | require.NoError(t, err) | |
| 430 | 430 | require.True(t, acquired, "the test must hold the spawn lock") | |
| 431 | 431 | t.Cleanup(release) | |
| 432 | + | // The daemon being started takes lifecycle ownership at once and keeps it | |
| 433 | + | // for its whole run, as the real daemon does. Without it, a caller that | |
| 434 | + | // dialed just before the daemon was published could win the spawn lock | |
| 435 | + | // once it is released and elect itself. | |
| 436 | + | daemonOwner, err := daemonLifecycleProbe.TryAcquire(filepath.Dir(carriage)) | |
| 437 | + | require.NoError(t, err) | |
| 438 | + | t.Cleanup(func() { _ = daemonOwner.Release() }) | |
| 432 | 439 | ||
| 433 | 440 | // The target is resolved once: require.* is not goroutine-safe, and the | |
| 434 | 441 | // resolved target is immutable and shared by every caller. | |
| @@ -429,6 +429,13 @@ func TestDaemonStartConcurrentRequestsShareOneElection(t *testing.T) { | |||||
| 429 | require.NoError(t, err) | 429 | require.NoError(t, err) | ||
| 430 | require.True(t, acquired, "the test must hold the spawn lock") | 430 | require.True(t, acquired, "the test must hold the spawn lock") | ||
| 431 | t.Cleanup(release) | 431 | t.Cleanup(release) | ||
| 432 | + | // The daemon being started takes lifecycle ownership at once and keeps it | |||
| 433 | + | // for its whole run, as the real daemon does. Without it, a caller that | |||
| 434 | + | // dialed just before the daemon was published could win the spawn lock | |||
| 435 | + | // once it is released and elect itself. | |||
| 436 | + | daemonOwner, err := daemonLifecycleProbe.TryAcquire(filepath.Dir(carriage)) | |||
| 437 | + | require.NoError(t, err) | |||
| 438 | + | t.Cleanup(func() { _ = daemonOwner.Release() }) | |||
| 432 | 439 | ||||
| 433 | // The target is resolved once: require.* is not goroutine-safe, and the | 440 | // The target is resolved once: require.* is not goroutine-safe, and the | ||
| 434 | // resolved target is immutable and shared by every caller. | 441 | // resolved target is immutable and shared by every caller. | ||
M internal/app/web_daemon_broker_test.go +3 -1
| @@ -635,7 +635,9 @@ func TestWebGatewayTabReturnsToPickerOnBrokerLoss(t *testing.T) { | |||
| 635 | 635 | t.Fatalf("broker loss ended the browser run: %v", tab.readerErr()) | |
| 636 | 636 | default: | |
| 637 | 637 | } | |
| 638 | - | require.GreaterOrEqual(t, calls.Load(), int32(2), "a lost connection must be retried, never fatal") | |
| 638 | + | // A repaint can land before the first reconnect attempt, so wait for it. | |
| 639 | + | require.Eventually(t, func() bool { return calls.Load() >= 2 }, brokerTestWait, 5*time.Millisecond, | |
| 640 | + | "a lost connection must be retried, never fatal") | |
| 639 | 641 | } | |
| 640 | 642 | ||
| 641 | 643 | // TestWebCompositionUsesTheBrokerConnector pins the production composition | |
| @@ -635,7 +635,9 @@ func TestWebGatewayTabReturnsToPickerOnBrokerLoss(t *testing.T) { | |||||
| 635 | t.Fatalf("broker loss ended the browser run: %v", tab.readerErr()) | 635 | t.Fatalf("broker loss ended the browser run: %v", tab.readerErr()) | ||
| 636 | default: | 636 | default: | ||
| 637 | } | 637 | } | ||
| 638 | - | require.GreaterOrEqual(t, calls.Load(), int32(2), "a lost connection must be retried, never fatal") | 638 | + | // A repaint can land before the first reconnect attempt, so wait for it. |
| 639 | + | require.Eventually(t, func() bool { return calls.Load() >= 2 }, brokerTestWait, 5*time.Millisecond, | |||
| 640 | + | "a lost connection must be retried, never fatal") | |||
| 639 | } | 641 | } | ||
| 640 | 642 | ||||
| 641 | // TestWebCompositionUsesTheBrokerConnector pins the production composition | 643 | // TestWebCompositionUsesTheBrokerConnector pins the production composition | ||