package push import ( "time" pushv1 "galaxy/backend/proto/push/v1" ) // ringEntry is one event stored in the in-memory replay buffer. The // cursor is duplicated here for O(1) comparison without re-parsing // event.Cursor. type ringEntry struct { cursor uint64 addedAt time.Time event *pushv1.PushEvent } // ring is the in-memory replay buffer. Entries are evicted by either // freshness-window TTL or capacity, whichever triggers first. The ring // is not safe for concurrent use; the owning Service serialises access // under its mutex. type ring struct { capacity int ttl time.Duration entries []ringEntry lastEvicted uint64 // largest cursor evicted from the buffer hasLastEvicted bool } func newRing(capacity int, ttl time.Duration) *ring { return &ring{ capacity: capacity, ttl: ttl, entries: make([]ringEntry, 0, capacity), } } // append records ev with its cursor and evicts entries past TTL or // capacity. The caller is responsible for setting ev.Cursor to // formatCursor(cursor) before calling. func (r *ring) append(cursor uint64, ev *pushv1.PushEvent, now time.Time) { r.evictExpired(now) for len(r.entries) >= r.capacity { r.evictHead() } r.entries = append(r.entries, ringEntry{cursor: cursor, addedAt: now, event: ev}) } // since returns the events with cursor strictly greater than fromCursor // in ascending cursor order. The boolean is true when the requested // cursor is "stale" — either older than the oldest retained event or // older than the last evicted cursor — meaning the caller missed at // least one event that the ring no longer holds. Stale callers receive // no replay and must resume from the live tail. func (r *ring) since(fromCursor uint64, now time.Time) ([]*pushv1.PushEvent, bool) { r.evictExpired(now) if len(r.entries) == 0 { // An empty ring is never stale: gateway is either fully caught // up or there has been no traffic. return nil, false } if r.hasLastEvicted && fromCursor < r.lastEvicted { return nil, true } first := r.entries[0].cursor if fromCursor+1 < first { return nil, true } out := make([]*pushv1.PushEvent, 0) for i := range r.entries { if r.entries[i].cursor > fromCursor { out = append(out, r.entries[i].event) } } return out, false } // len reports the current number of retained entries; intended for // tests and metrics. func (r *ring) len() int { return len(r.entries) } func (r *ring) evictExpired(now time.Time) { if r.ttl <= 0 { return } cutoff := now.Add(-r.ttl) drop := 0 for drop < len(r.entries) && r.entries[drop].addedAt.Before(cutoff) { drop++ } if drop == 0 { return } r.lastEvicted = r.entries[drop-1].cursor r.hasLastEvicted = true r.entries = append(r.entries[:0], r.entries[drop:]...) } func (r *ring) evictHead() { if len(r.entries) == 0 { return } r.lastEvicted = r.entries[0].cursor r.hasLastEvicted = true r.entries = append(r.entries[:0], r.entries[1:]...) }