feat: backend service
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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:]...)
|
||||
}
|
||||
Reference in New Issue
Block a user