package intent import ( "time" ) // Box presence from the wait channel (R-30, design `audits/day-2026-10-08/design-R-30.md` option B). // // A healthy controller holds GET /api/v1/wait for waitMaxHold (240 s) and starts the next hold at // once; measured on the ingress 2026-10-08: one new wait per box every 241–243 s. So "a hold is open, // or one STARTED less than PresenceConnectedWindow ago" means the box's controller is running and // reaching the hub. A box that loses power ends its hold normally (the hub ends it at 240 s) and then // no new wait arrives — that gap is the signal. // // IN MEMORY ONLY, like the generation counter. A hub that started less than PresenceConnectedWindow // ago has not had the chance to see a healthy box's next wait, so it answers PresenceUnknown — never // PresenceNotConnected. Callers fall back to the report clock on Unknown (the safe direction). // Pinned by presence_test.go TestPresence_HubRestartIsUnknown. const ( // PresenceWaitCadence is the measured interval between two wait starts from one healthy box. PresenceWaitCadence = 243 * time.Second // PresenceGrace absorbs a slow reconnect (TLS, DNS, a busy box) on top of the cadence. PresenceGrace = 90 * time.Second // PresenceConnectedWindow = cadence + grace (333 s): a wait started within it means "connected". PresenceConnectedWindow = PresenceWaitCadence + PresenceGrace ) // Presence states. const ( PresenceConnected = "connected" PresenceNotConnected = "not_connected" PresenceUnknown = "unknown" ) // Presence is one customer's box-connection verdict at a moment. type Presence struct { State string // Since is the start of the not-connected span (the last hold's end, or the hub's own start when // no wait was seen since). Zero unless State == PresenceNotConnected. Since time.Time // NeverSeen: no wait from this customer since the hub started (Since is then the hub's start). NeverSeen bool } // NotConnectedFor is how long the box has been without a connection at now (0 unless not connected). func (p Presence) NotConnectedFor(now time.Time) time.Duration { if p.State != PresenceNotConnected { return 0 } if d := now.Sub(p.Since); d > 0 { return d } return 0 } type presenceRec struct { open int lastStart time.Time lastEnd time.Time lastState string // for the debug line on a change } func (h *Hub) clock() time.Time { if h.now != nil { return h.now() } return time.Now() } // SetClock replaces the presence clock (tests). Must be called before use; also resets the hub's // start time to the new clock's now. func (h *Hub) SetClock(now func() time.Time) { h.mu.Lock() defer h.mu.Unlock() h.now = now h.born = h.clock() } // SetLogger wires a Printf-style logger for the presence debug lines. Nil = silent. func (h *Hub) SetLogger(logf func(format string, args ...any)) { h.mu.Lock() defer h.mu.Unlock() h.logf = logf } func (h *Hub) rec(customerID string) *presenceRec { r := h.presence[customerID] if r == nil { r = &presenceRec{} h.presence[customerID] = r } return r } // MarkWaitStart records that a wait request from the customer's box began. Called ONCE per HTTP // request by the wait handler (not inside Wait, which runs once per heartbeat window). func (h *Hub) MarkWaitStart(customerID string) { if customerID == "" { return } h.mu.Lock() defer h.mu.Unlock() now := h.clock() r := h.rec(customerID) r.open++ r.lastStart = now h.noteStateLocked(customerID, r, now) } // MarkWaitEnd records that a wait request ended (any exit path). func (h *Hub) MarkWaitEnd(customerID string) { if customerID == "" { return } h.mu.Lock() defer h.mu.Unlock() r := h.rec(customerID) if r.open > 0 { r.open-- } r.lastEnd = h.clock() } // Presence answers the customer's box-connection verdict at now. func (h *Hub) Presence(customerID string, now time.Time) Presence { if customerID == "" { return Presence{State: PresenceUnknown} } h.mu.Lock() defer h.mu.Unlock() r := h.presence[customerID] p := h.presenceLocked(r, now) if r != nil { h.noteStateLocked(customerID, r, now) } return p } func (h *Hub) presenceLocked(r *presenceRec, now time.Time) Presence { if r != nil { if r.open > 0 || (!r.lastStart.IsZero() && now.Sub(r.lastStart) < PresenceConnectedWindow) { return Presence{State: PresenceConnected} } } // The hub itself has not been up long enough to have seen a healthy box's next wait. if now.Sub(h.born) < PresenceConnectedWindow { return Presence{State: PresenceUnknown} } if r == nil || r.lastStart.IsZero() { return Presence{State: PresenceNotConnected, Since: h.born, NeverSeen: true} } since := r.lastEnd if since.Before(r.lastStart) { since = r.lastStart } return Presence{State: PresenceNotConnected, Since: since} } // noteStateLocked writes one DEBUG line when a customer's presence verdict changes. func (h *Hub) noteStateLocked(customerID string, r *presenceRec, now time.Time) { st := h.presenceLocked(r, now).State if st == r.lastState { return } prev := r.lastState r.lastState = st if h.logf != nil && prev != "" { h.logf("[DEBUG] box presence %s: %s -> %s (open holds %d)", customerID, prev, st, r.open) } }