diff options
Diffstat (limited to 'internal/engine')
| -rw-r--r-- | internal/engine/tick.go | 17 |
1 files changed, 14 insertions, 3 deletions
diff --git a/internal/engine/tick.go b/internal/engine/tick.go index aa10a73..68dd54f 100644 --- a/internal/engine/tick.go +++ b/internal/engine/tick.go @@ -26,6 +26,7 @@ type Engine struct { ticker *time.Ticker running bool stopCh chan struct{} + loopDone chan struct{} } func New() *Engine { @@ -67,8 +68,11 @@ func (e *Engine) Start(tickLengthMs int) { // Capture this run's resources. A quick Stop/Start must not make the // old goroutine consume the replacement ticker or stop channel. ticker, stopCh := e.ticker, e.stopCh + loopDone := make(chan struct{}) + e.loopDone = loopDone go func() { + defer close(loopDone) for { select { case <-ticker.C: @@ -100,10 +104,17 @@ func (e *Engine) Stop() { // exited. Use this for graceful shutdown, where the final tick must finish // before owned state is saved. func (e *Engine) StopAndWait() { + e.mu.Lock() + loopDone := e.loopDone + e.mu.Unlock() e.Stop() - // Join any in-flight tick: processTick holds tickMu for its whole body, - // so acquiring the mutex blocks until the running callback completes; - // holding it until return keeps that guarantee for the caller. + // Wait for the ticker loop goroutine to exit. This is the real save + // barrier: the loop may already have received from the ticker and be + // about to call processTick, which locking tickMu alone would not catch. + if loopDone != nil { + <-loopDone + } + // Also join a direct Tick() that is mid-callback on another goroutine. e.tickMu.Lock() defer e.tickMu.Unlock() } |
