mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
refactor(18-graceful-shutdown): harden mock shutdown lifecycle in internal/mock
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
var (
|
||||
dataMu sync.RWMutex
|
||||
setEnabledMu sync.Mutex
|
||||
mockData models.StateSnapshot
|
||||
mockAlerts []models.Alert
|
||||
mockConfig = DefaultConfig
|
||||
@@ -44,6 +45,9 @@ func SetEnabled(enable bool) {
|
||||
}
|
||||
|
||||
func setEnabled(enable bool, fromInit bool) {
|
||||
setEnabledMu.Lock()
|
||||
defer setEnabledMu.Unlock()
|
||||
|
||||
current := enabled.Load()
|
||||
if current == enable {
|
||||
// Still update env so other processes see the latest value when not invoked from init.
|
||||
@@ -111,7 +115,12 @@ func disableMockMode() {
|
||||
return
|
||||
}
|
||||
enabled.Store(false)
|
||||
stopUpdateLoopLocked()
|
||||
stopUpdateLoopSignalLocked()
|
||||
dataMu.Unlock()
|
||||
|
||||
waitForUpdateLoopStop()
|
||||
|
||||
dataMu.Lock()
|
||||
mockData = models.StateSnapshot{}
|
||||
mockAlerts = nil
|
||||
dataMu.Unlock()
|
||||
@@ -142,6 +151,11 @@ func startUpdateLoopLocked() {
|
||||
}
|
||||
|
||||
func stopUpdateLoopLocked() {
|
||||
stopUpdateLoopSignalLocked()
|
||||
waitForUpdateLoopStop()
|
||||
}
|
||||
|
||||
func stopUpdateLoopSignalLocked() {
|
||||
if ch := stopUpdatesCh; ch != nil {
|
||||
close(ch)
|
||||
stopUpdatesCh = nil
|
||||
@@ -150,7 +164,9 @@ func stopUpdateLoopLocked() {
|
||||
ticker.Stop()
|
||||
updateTicker = nil
|
||||
}
|
||||
// Wait for the update goroutine to exit
|
||||
}
|
||||
|
||||
func waitForUpdateLoopStop() {
|
||||
updateLoopWg.Wait()
|
||||
}
|
||||
|
||||
|
||||
38
internal/mock/integration_shutdown_test.go
Normal file
38
internal/mock/integration_shutdown_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSetEnabledDisableDoesNotDeadlockWhenLoopNeedsStateLock(t *testing.T) {
|
||||
SetEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
SetEnabled(false)
|
||||
})
|
||||
|
||||
dataMu.Lock()
|
||||
enabled.Store(true)
|
||||
stopUpdatesCh = make(chan struct{})
|
||||
updateTicker = time.NewTicker(time.Hour)
|
||||
updateLoopWg.Add(1)
|
||||
go func(stop <-chan struct{}) {
|
||||
defer updateLoopWg.Done()
|
||||
<-stop
|
||||
dataMu.Lock()
|
||||
dataMu.Unlock()
|
||||
}(stopUpdatesCh)
|
||||
dataMu.Unlock()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
SetEnabled(false)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("SetEnabled(false) timed out waiting for update loop shutdown")
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/license"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/license/conversion"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/logging"
|
||||
_ "github.com/rcourtman/pulse-go-rewrite/internal/mock" // Import for init() to run
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
|
||||
@@ -443,6 +443,11 @@ shutdown:
|
||||
// Stop AI chat service (kills sidecar process group)
|
||||
router.StopAIChat(shutdownCtx)
|
||||
|
||||
// Ensure mock-mode background update ticker is stopped before process exit.
|
||||
if mock.IsMockEnabled() {
|
||||
mock.SetEnabled(false)
|
||||
}
|
||||
|
||||
cancel()
|
||||
reloadableMonitor.Stop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user