app_monitor.go RawHistoryFolder
1package app
2
3import (
4 "context"
5 "sync"
6 "time"
7
8 "github.com/bnema/zerowrap"
9
10 "github.com/bnema/gordon/internal/boundaries/in"
11)
12
13// appMonitorInterval is the fixed periodic recovery cadence.
14const appMonitorInterval = 15 * time.Second
15
16// appMonitor is the daemon-owned driver of periodic app recovery. One
17// instance lives for the whole process: the loop is created once after
18// boot reconciliation and reload never starts another. Passes never
19// overlap because a single goroutine runs them.
20type appMonitor struct {
21 reconciler in.AppReconciler
22 interval time.Duration
23 // ticks lets tests drive the cadence deterministically. Nil uses a
24 // real ticker.
25 ticks func() <-chan time.Time
26 log zerowrap.Logger
27
28 mu sync.Mutex
29 cancel context.CancelFunc
30 done chan struct{}
31}
32
33func newAppMonitor(reconciler in.AppReconciler, log zerowrap.Logger) *appMonitor {
34 return &appMonitor{reconciler: reconciler, interval: appMonitorInterval, log: log}
35}
36
37// Start launches the recovery loop under the daemon context. It is
38// idempotent: a running monitor is left untouched.
39func (m *appMonitor) Start(ctx context.Context) {
40 if m == nil || m.reconciler == nil {
41 return
42 }
43 m.mu.Lock()
44 defer m.mu.Unlock()
45 if m.cancel != nil {
46 return
47 }
48 runCtx, cancel := context.WithCancel(ctx)
49 m.cancel = cancel
50 done := make(chan struct{})
51 m.done = done
52 go m.loop(runCtx, done)
53}
54
55// Stop cancels the loop and joins it, so shutdown never races runtime,
56// traffic, or state teardown. Safe to call more than once.
57func (m *appMonitor) Stop() {
58 if m == nil {
59 return
60 }
61 m.mu.Lock()
62 cancel, done := m.cancel, m.done
63 m.cancel, m.done = nil, nil
64 m.mu.Unlock()
65 if cancel == nil {
66 return
67 }
68 cancel()
69 <-done
70}
71
72func (m *appMonitor) loop(ctx context.Context, done chan struct{}) {
73 defer close(done)
74 var ticks <-chan time.Time
75 if m.ticks != nil {
76 ticks = m.ticks()
77 } else {
78 ticker := time.NewTicker(m.interval)
79 defer ticker.Stop()
80 ticks = ticker.C
81 }
82 for {
83 select {
84 case <-ctx.Done():
85 return
86 case <-ticks:
87 if err := m.reconciler.ReconcileRunning(ctx); err != nil {
88 m.log.Warn().Err(err).Msg("app reconciliation pass failed")
89 }
90 }
91 }
92}