refactor(monitor): encapsulate engine state, add graceful shutdown and tests
Replace all monitor package-level mutable state with Engine struct. All state (liveState, logStore, histories, tokenIndex, HTTP clients) is now encapsulated in Engine, created via NewEngine(store). Key changes: - Engine struct holds all monitor state with proper mutex protection - Engine.Start(ctx) and monitorRoutine respect context cancellation for graceful shutdown — no more leaked goroutines - cluster.runFollowerLoop also respects context for clean exit - Token index (map[string]int) for O(1) push heartbeat lookup, replacing O(n) linear scan through LiveState - UpdateSiteConfig preserves 8 runtime fields instead of copying 17 config fields individually - triggerAlert goroutines get 30s timeout context - All consumers (TUI, server, cluster, main) receive *Engine via constructor/parameter — no package-level state access - main.go creates context.WithCancel, passes to engine and cluster First test suite: 12 tests across store and alert packages - Store: CRUD for sites/alerts/users, push token generation, import/export round-trip, check history persistence - Alert: Discord/Slack/Webhook payload format, HTTP 4xx error propagation, Ntfy headers, unknown provider returns nil
This commit is contained in:
@@ -148,7 +148,7 @@ type ServerConfig struct {
|
||||
ClusterKey string // Shared Secret for Security
|
||||
}
|
||||
|
||||
func Start(cfg ServerConfig, s store.Store) {
|
||||
func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) {
|
||||
if cfg.ClusterKey == "" {
|
||||
fmt.Println("WARNING: No UPKEEP_CLUSTER_SECRET set. Cluster API endpoints are unauthenticated.")
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func Start(cfg ServerConfig, s store.Store) {
|
||||
http.Error(w, "Missing token", 400)
|
||||
return
|
||||
}
|
||||
if monitor.RecordHeartbeat(token) {
|
||||
if eng.RecordHeartbeat(token) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
@@ -244,12 +244,10 @@ func Start(cfg ServerConfig, s store.Store) {
|
||||
|
||||
// 6. Status Page
|
||||
if cfg.EnableStatus {
|
||||
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { renderStatusPage(w, cfg.Title) })
|
||||
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { renderStatusPage(w, cfg.Title, eng) })
|
||||
mux.HandleFunc("/status/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
monitor.Mutex.RLock()
|
||||
defer monitor.Mutex.RUnlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(monitor.LiveState)
|
||||
json.NewEncoder(w).Encode(eng.GetLiveState())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -262,13 +260,8 @@ func Start(cfg ServerConfig, s store.Store) {
|
||||
}()
|
||||
}
|
||||
|
||||
func renderStatusPage(w http.ResponseWriter, title string) {
|
||||
monitor.Mutex.RLock()
|
||||
var sites []models.Site
|
||||
for _, s := range monitor.LiveState {
|
||||
sites = append(sites, s)
|
||||
}
|
||||
monitor.Mutex.RUnlock()
|
||||
func renderStatusPage(w http.ResponseWriter, title string, eng *monitor.Engine) {
|
||||
sites := eng.GetAllSites()
|
||||
|
||||
sort.Slice(sites, func(i, j int) bool {
|
||||
if sites[i].Status != sites[j].Status {
|
||||
|
||||
Reference in New Issue
Block a user