package monitor import "context" import "sync" import "time" import "codit/internal/db" const summaryRefreshInterval time.Duration = 30 * time.Second const summaryTTL time.Duration = 90 * time.Second // MonitorSummary holds the expensive derived per-monitor field: uptime over the // 24h/30d windows, keyed by public id as [24h, 30d]. (Recent-beats bars were // dropped from the list payload; the detail view fetches full checks instead.) type MonitorSummary struct { Uptime map[string][2]float64 } // SummaryCache serves the derived monitor summary on a slower cadence than the // list is polled: a background loop refreshes it every summaryRefreshInterval // (proactive), and a read that finds it older than summaryTTL triggers a // single-flight refresh (lazy safety net). Fresh per-request fields (state, // latency) are NOT here — only the costly aggregates. type SummaryCache struct { store *db.Store ttl time.Duration mu sync.RWMutex snap MonitorSummary computedAt time.Time refreshMu sync.Mutex } func NewSummaryCache(store *db.Store, ttl time.Duration) *SummaryCache { return &SummaryCache{store: store, ttl: ttl} } // Refresh recomputes the summary from the store and replaces the snapshot. func (c *SummaryCache) Refresh(ctx context.Context) error { var now int64 var uptime map[string][2]float64 var err error now = time.Now().UTC().Unix() uptime, err = c.store.MonitorUptimeAll(now) if err != nil { return err } c.mu.Lock() c.snap = MonitorSummary{Uptime: uptime} c.computedAt = time.Now() c.mu.Unlock() return nil } // Snapshot returns the cached summary, refreshing synchronously (single-flight) // when the cache is empty or older than the TTL. If another goroutine is already // refreshing, the current (possibly slightly stale) snapshot is returned. func (c *SummaryCache) Snapshot(ctx context.Context) MonitorSummary { var snap MonitorSummary var age time.Duration c.mu.RLock() snap = c.snap age = time.Since(c.computedAt) c.mu.RUnlock() if snap.Uptime != nil && age <= c.ttl { return snap } if c.refreshMu.TryLock() { _ = c.Refresh(ctx) c.refreshMu.Unlock() c.mu.RLock() snap = c.snap c.mu.RUnlock() } return snap }