fix(daemon): complete kill-all shutdown handoff
- commit
- 9d942b66e586fe11b52e109be1cf0e1d48489d94
- author
- brice · · 2026-08-01 07:52 UTC
- parent
- 05679c2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +33 -0
M internal/usecase/daemon/daemon.go +3 -0
| @@ -835,6 +835,9 @@ func (d *Daemon) shutdownAllWithSnapshotDeadline(reason uint8, deadline *snapsho | |||
| 835 | 835 | } | |
| 836 | 836 | } | |
| 837 | 837 | } | |
| 838 | + | // Signal Serve even when a concurrent role transition made this initial | |
| 839 | + | // pass abort; Serve owns the bounded defensive teardown passes. | |
| 840 | + | d.doneOnce.Do(func() { close(d.done) }) | |
| 838 | 841 | return checkpointIncomplete | |
| 839 | 842 | } | |
| 840 | 843 | ||
| @@ -835,6 +835,9 @@ func (d *Daemon) shutdownAllWithSnapshotDeadline(reason uint8, deadline *snapsho | |||||
| 835 | } | 835 | } | ||
| 836 | } | 836 | } | ||
| 837 | } | 837 | } | ||
| 838 | + | // Signal Serve even when a concurrent role transition made this initial | |||
| 839 | + | // pass abort; Serve owns the bounded defensive teardown passes. | |||
| 840 | + | d.doneOnce.Do(func() { close(d.done) }) | |||
| 838 | return checkpointIncomplete | 841 | return checkpointIncomplete | ||
| 839 | } | 842 | } | ||
| 840 | 843 | ||||
M internal/usecase/daemon/role_effect_gate_test.go +30 -0
| @@ -353,6 +353,36 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test | |||
| 353 | 353 | requireRoleGateRetired(t, ac) | |
| 354 | 354 | } | |
| 355 | 355 | ||
| 356 | + | func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T) { | |
| 357 | + | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 358 | + | transport := &closeTrackingTransport{} | |
| 359 | + | active.replaceTransport(transport) | |
| 360 | + | d.attachCoordinator(sess, nil, active, true) | |
| 361 | + | active.publishRoleCapability(sess.attachmentToken(active, transport)) | |
| 362 | + | ||
| 363 | + | owner := freezeRoleEffectGates(active) | |
| 364 | + | require.True(t, owner.acquired) | |
| 365 | + | defer owner.unfreeze() | |
| 366 | + | ||
| 367 | + | clock := &signalClock{timers: make(chan *signalTimer, 1)} | |
| 368 | + | d.clock = clock | |
| 369 | + | shutdownDone := make(chan bool, 1) | |
| 370 | + | go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }() | |
| 371 | + | ||
| 372 | + | deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline") | |
| 373 | + | deadline.ch <- time.Now() | |
| 374 | + | require.False(t, awaitTestValue(t, shutdownDone, "shutdown did not return after its gate acquisition deadline")) | |
| 375 | + | ||
| 376 | + | d.mu.Lock() | |
| 377 | + | require.Same(t, sess, d.sessions[sess.id], "timed-out initial teardown should leave the session for Serve's shutdown pass") | |
| 378 | + | d.mu.Unlock() | |
| 379 | + | select { | |
| 380 | + | case <-d.done: | |
| 381 | + | case <-time.After(time.Second): | |
| 382 | + | t.Fatal("shutdown request did not signal Serve after the initial teardown pass timed out") | |
| 383 | + | } | |
| 384 | + | } | |
| 385 | + | ||
| 356 | 386 | func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | |
| 357 | 387 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |
| 358 | 388 | blocked := newTeardownBlockingTransport() | |
| @@ -353,6 +353,36 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test | |||||
| 353 | requireRoleGateRetired(t, ac) | 353 | requireRoleGateRetired(t, ac) | ||
| 354 | } | 354 | } | ||
| 355 | 355 | ||||
| 356 | + | func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T) { | |||
| 357 | + | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | |||
| 358 | + | transport := &closeTrackingTransport{} | |||
| 359 | + | active.replaceTransport(transport) | |||
| 360 | + | d.attachCoordinator(sess, nil, active, true) | |||
| 361 | + | active.publishRoleCapability(sess.attachmentToken(active, transport)) | |||
| 362 | + | ||||
| 363 | + | owner := freezeRoleEffectGates(active) | |||
| 364 | + | require.True(t, owner.acquired) | |||
| 365 | + | defer owner.unfreeze() | |||
| 366 | + | ||||
| 367 | + | clock := &signalClock{timers: make(chan *signalTimer, 1)} | |||
| 368 | + | d.clock = clock | |||
| 369 | + | shutdownDone := make(chan bool, 1) | |||
| 370 | + | go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }() | |||
| 371 | + | ||||
| 372 | + | deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline") | |||
| 373 | + | deadline.ch <- time.Now() | |||
| 374 | + | require.False(t, awaitTestValue(t, shutdownDone, "shutdown did not return after its gate acquisition deadline")) | |||
| 375 | + | ||||
| 376 | + | d.mu.Lock() | |||
| 377 | + | require.Same(t, sess, d.sessions[sess.id], "timed-out initial teardown should leave the session for Serve's shutdown pass") | |||
| 378 | + | d.mu.Unlock() | |||
| 379 | + | select { | |||
| 380 | + | case <-d.done: | |||
| 381 | + | case <-time.After(time.Second): | |||
| 382 | + | t.Fatal("shutdown request did not signal Serve after the initial teardown pass timed out") | |||
| 383 | + | } | |||
| 384 | + | } | |||
| 385 | + | ||||
| 356 | func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | 386 | func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | ||
| 357 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | 387 | d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{}) | ||
| 358 | blocked := newTeardownBlockingTransport() | 388 | blocked := newTeardownBlockingTransport() | ||