test(daemon): cover reentrant coordinator timers
- commit
- 9c054e2e9934ac05133b209226e7e51383473051
- author
- brice · · 2026-07-11 19:51 UTC
- committer
- brice · · 2026-07-11 19:51 UTC
- parent
- de2ad79
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +31 -0
M internal/usecase/daemon/render_coordinator_test.go +31 -0
| @@ -73,6 +73,37 @@ func newInertCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockCl | |||
| 73 | 73 | return clk | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | + | // TestRenderCoordinatorTimerCallbacksReenterCoordinator is deliberately a | |
| 77 | + | // deadlock regression: every external timer method synchronously reads | |
| 78 | + | // coordinator metadata. Before two-phase timer ownership this blocked while | |
| 79 | + | // c.mu was held during normal arming, urgent replacement, sync arming, and | |
| 80 | + | // detach cancellation. | |
| 81 | + | func TestRenderCoordinatorTimerCallbacksReenterCoordinator(t *testing.T) { | |
| 82 | + | clock := portsmocks.NewMockClock(t) | |
| 83 | + | var rc *renderCoordinator | |
| 84 | + | clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer { | |
| 85 | + | timer := portsmocks.NewMockTimer(t) | |
| 86 | + | ch := make(chan time.Time) | |
| 87 | + | timer.EXPECT().C().Run(func() { | |
| 88 | + | _ = rc.resizeSnapshot() | |
| 89 | + | _ = rc.burstMetricsSnapshot() | |
| 90 | + | }).Return((<-chan time.Time)(ch)).Once() | |
| 91 | + | timer.EXPECT().Stop().Run(func() { | |
| 92 | + | _ = rc.resizeSnapshot() | |
| 93 | + | _ = rc.burstMetricsSnapshot() | |
| 94 | + | }).Return(true).Once() | |
| 95 | + | return timer | |
| 96 | + | }).Times(3) | |
| 97 | + | rc = newRenderCoordinator(renderCoordinatorOptions{clock: clock}) | |
| 98 | + | ac := &attachedClient{} | |
| 99 | + | rc.attach(ac) | |
| 100 | + | ||
| 101 | + | rc.invalidate(renderInvalidation{class: invalidateOutput}) // normal arm | |
| 102 | + | rc.invalidate(renderInvalidation{class: invalidateUrgent}) // urgent promotion | |
| 103 | + | rc.noteSyncBegin(nil, 1) // sync arm | |
| 104 | + | rc.noteDetach(ac) // cancel and stop | |
| 105 | + | } | |
| 106 | + | ||
| 76 | 107 | // coordinatorHarness wires one coordinator to generated clock/timer mocks and | |
| 77 | 108 | // recording hooks. Every assertion is channel- or counter-based; nothing sleeps. | |
| 78 | 109 | type coordinatorHarness struct { | |
| @@ -73,6 +73,37 @@ func newInertCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockCl | |||||
| 73 | return clk | 73 | return clk | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | + | // TestRenderCoordinatorTimerCallbacksReenterCoordinator is deliberately a | |||
| 77 | + | // deadlock regression: every external timer method synchronously reads | |||
| 78 | + | // coordinator metadata. Before two-phase timer ownership this blocked while | |||
| 79 | + | // c.mu was held during normal arming, urgent replacement, sync arming, and | |||
| 80 | + | // detach cancellation. | |||
| 81 | + | func TestRenderCoordinatorTimerCallbacksReenterCoordinator(t *testing.T) { | |||
| 82 | + | clock := portsmocks.NewMockClock(t) | |||
| 83 | + | var rc *renderCoordinator | |||
| 84 | + | clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer { | |||
| 85 | + | timer := portsmocks.NewMockTimer(t) | |||
| 86 | + | ch := make(chan time.Time) | |||
| 87 | + | timer.EXPECT().C().Run(func() { | |||
| 88 | + | _ = rc.resizeSnapshot() | |||
| 89 | + | _ = rc.burstMetricsSnapshot() | |||
| 90 | + | }).Return((<-chan time.Time)(ch)).Once() | |||
| 91 | + | timer.EXPECT().Stop().Run(func() { | |||
| 92 | + | _ = rc.resizeSnapshot() | |||
| 93 | + | _ = rc.burstMetricsSnapshot() | |||
| 94 | + | }).Return(true).Once() | |||
| 95 | + | return timer | |||
| 96 | + | }).Times(3) | |||
| 97 | + | rc = newRenderCoordinator(renderCoordinatorOptions{clock: clock}) | |||
| 98 | + | ac := &attachedClient{} | |||
| 99 | + | rc.attach(ac) | |||
| 100 | + | ||||
| 101 | + | rc.invalidate(renderInvalidation{class: invalidateOutput}) // normal arm | |||
| 102 | + | rc.invalidate(renderInvalidation{class: invalidateUrgent}) // urgent promotion | |||
| 103 | + | rc.noteSyncBegin(nil, 1) // sync arm | |||
| 104 | + | rc.noteDetach(ac) // cancel and stop | |||
| 105 | + | } | |||
| 106 | + | ||||
| 76 | // coordinatorHarness wires one coordinator to generated clock/timer mocks and | 107 | // coordinatorHarness wires one coordinator to generated clock/timer mocks and | ||
| 77 | // recording hooks. Every assertion is channel- or counter-based; nothing sleeps. | 108 | // recording hooks. Every assertion is channel- or counter-based; nothing sleeps. | ||
| 78 | type coordinatorHarness struct { | 109 | type coordinatorHarness struct { | ||