test(app): align QUIC proxy teardown with active readers (#278)
- commit
- a92a4c3f4b2b20f75257dd5ae1ef66f3e7ab5b62
- author
- bnema · · 2026-09-14 19:49 UTC
- committer
- GitHub · · 2026-09-14 19:49 UTC
- parent
- 1fedcbd
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
1 file · +27 -10
M internal/app/run_test.go +27 -10
| @@ -436,10 +436,9 @@ func TestProxyTransportsCopiesBothDirections(t *testing.T) { | |||
| 436 | 436 | } | |
| 437 | 437 | ||
| 438 | 438 | // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy | |
| 439 | - | // bridge instead of a bare same-process Transport.Close: the daemon direction | |
| 440 | - | // emits one final synchronous envelope, the proxy forwards it, tears both | |
| 441 | - | // carriages down, and then performs the bounded graceful teardown wait the | |
| 442 | - | // detached _quic-proxy uses before process exit. The envelope must survive. | |
| 439 | + | // bridge instead of a bare same-process Transport.Close. It verifies that a | |
| 440 | + | // client already reading, as production clients are, receives the daemon's | |
| 441 | + | // final synchronous envelope while the proxy tears both carriages down. | |
| 443 | 442 | func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) { | |
| 444 | 443 | cert, fingerprint, err := quic.GenerateEphemeralCert() | |
| 445 | 444 | require.NoError(t, err) | |
| @@ -479,19 +478,37 @@ func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) { | |||
| 479 | 478 | require.NoError(t, peer.Send(wire.Envelope{Payload: final})) | |
| 480 | 479 | _ = peer.Close() | |
| 481 | 480 | ||
| 481 | + | // Receive while the proxy performs its bounded graceful teardown. Waiting | |
| 482 | + | // for teardown first lets QUIC's eventual CONNECTION_CLOSE discard unread | |
| 483 | + | // stream data, a property the adapter does not promise to prevent. | |
| 484 | + | type recvResult struct { | |
| 485 | + | envelope wire.Envelope | |
| 486 | + | err error | |
| 487 | + | } | |
| 488 | + | received := make(chan recvResult, 1) | |
| 489 | + | go func() { | |
| 490 | + | envelope, err := client.Recv() | |
| 491 | + | received <- recvResult{envelope: envelope, err: err} | |
| 492 | + | }() | |
| 493 | + | var got wire.Envelope | |
| 494 | + | select { | |
| 495 | + | case result := <-received: | |
| 496 | + | require.NoError(t, result.err, "a reading client must receive the final envelope during proxy teardown") | |
| 497 | + | got = result.envelope | |
| 498 | + | case <-ctx.Done(): | |
| 499 | + | t.Fatal("timed out receiving the final envelope during proxy teardown") | |
| 500 | + | } | |
| 501 | + | require.Equal(t, final, got.Payload) | |
| 502 | + | ||
| 482 | 503 | select { | |
| 483 | 504 | case <-proxyDone: | |
| 484 | 505 | case <-time.After(10 * time.Second): | |
| 485 | 506 | t.Fatal("proxy did not stop after the daemon side closed") | |
| 486 | 507 | } | |
| 487 | 508 | ||
| 488 | - | // The proxy waits for deferred graceful teardown before returning; this is | |
| 489 | - | // what keeps the final envelope from being discarded at process exit. | |
| 509 | + | // Verify the bounded teardown phase completed after the proxy closed its | |
| 510 | + | // QUIC carriage; runQUICProxy performs the same wait before process exit. | |
| 490 | 511 | require.NoError(t, waitGracefulTeardown(proxySide)) | |
| 491 | - | ||
| 492 | - | got, err := client.Recv() | |
| 493 | - | require.NoError(t, err, "the final envelope must survive proxy teardown") | |
| 494 | - | require.Equal(t, final, got.Payload) | |
| 495 | 512 | _, err = client.Recv() | |
| 496 | 513 | require.ErrorIs(t, err, io.EOF) | |
| 497 | 514 | } | |
| @@ -436,10 +436,9 @@ func TestProxyTransportsCopiesBothDirections(t *testing.T) { | |||||
| 436 | } | 436 | } | ||
| 437 | 437 | ||||
| 438 | // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy | 438 | // TestQUICProxyLifecycleDeliversFinalEnvelope exercises the production proxy | ||
| 439 | - | // bridge instead of a bare same-process Transport.Close: the daemon direction | 439 | + | // bridge instead of a bare same-process Transport.Close. It verifies that a |
| 440 | - | // emits one final synchronous envelope, the proxy forwards it, tears both | 440 | + | // client already reading, as production clients are, receives the daemon's |
| 441 | - | // carriages down, and then performs the bounded graceful teardown wait the | 441 | + | // final synchronous envelope while the proxy tears both carriages down. |
| 442 | - | // detached _quic-proxy uses before process exit. The envelope must survive. | |||
| 443 | func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) { | 442 | func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) { | ||
| 444 | cert, fingerprint, err := quic.GenerateEphemeralCert() | 443 | cert, fingerprint, err := quic.GenerateEphemeralCert() | ||
| 445 | require.NoError(t, err) | 444 | require.NoError(t, err) | ||
| @@ -479,19 +478,37 @@ func TestQUICProxyLifecycleDeliversFinalEnvelope(t *testing.T) { | |||||
| 479 | require.NoError(t, peer.Send(wire.Envelope{Payload: final})) | 478 | require.NoError(t, peer.Send(wire.Envelope{Payload: final})) | ||
| 480 | _ = peer.Close() | 479 | _ = peer.Close() | ||
| 481 | 480 | ||||
| 481 | + | // Receive while the proxy performs its bounded graceful teardown. Waiting | |||
| 482 | + | // for teardown first lets QUIC's eventual CONNECTION_CLOSE discard unread | |||
| 483 | + | // stream data, a property the adapter does not promise to prevent. | |||
| 484 | + | type recvResult struct { | |||
| 485 | + | envelope wire.Envelope | |||
| 486 | + | err error | |||
| 487 | + | } | |||
| 488 | + | received := make(chan recvResult, 1) | |||
| 489 | + | go func() { | |||
| 490 | + | envelope, err := client.Recv() | |||
| 491 | + | received <- recvResult{envelope: envelope, err: err} | |||
| 492 | + | }() | |||
| 493 | + | var got wire.Envelope | |||
| 494 | + | select { | |||
| 495 | + | case result := <-received: | |||
| 496 | + | require.NoError(t, result.err, "a reading client must receive the final envelope during proxy teardown") | |||
| 497 | + | got = result.envelope | |||
| 498 | + | case <-ctx.Done(): | |||
| 499 | + | t.Fatal("timed out receiving the final envelope during proxy teardown") | |||
| 500 | + | } | |||
| 501 | + | require.Equal(t, final, got.Payload) | |||
| 502 | + | ||||
| 482 | select { | 503 | select { | ||
| 483 | case <-proxyDone: | 504 | case <-proxyDone: | ||
| 484 | case <-time.After(10 * time.Second): | 505 | case <-time.After(10 * time.Second): | ||
| 485 | t.Fatal("proxy did not stop after the daemon side closed") | 506 | t.Fatal("proxy did not stop after the daemon side closed") | ||
| 486 | } | 507 | } | ||
| 487 | 508 | ||||
| 488 | - | // The proxy waits for deferred graceful teardown before returning; this is | 509 | + | // Verify the bounded teardown phase completed after the proxy closed its |
| 489 | - | // what keeps the final envelope from being discarded at process exit. | 510 | + | // QUIC carriage; runQUICProxy performs the same wait before process exit. |
| 490 | require.NoError(t, waitGracefulTeardown(proxySide)) | 511 | require.NoError(t, waitGracefulTeardown(proxySide)) | ||
| 491 | - | ||||
| 492 | - | got, err := client.Recv() | |||
| 493 | - | require.NoError(t, err, "the final envelope must survive proxy teardown") | |||
| 494 | - | require.Equal(t, final, got.Payload) | |||
| 495 | _, err = client.Recv() | 512 | _, err = client.Recv() | ||
| 496 | require.ErrorIs(t, err, io.EOF) | 513 | require.ErrorIs(t, err, io.EOF) | ||
| 497 | } | 514 | } | ||