panel — open-source game server manager (public release)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package events
|
||||
|
||||
// Replay ring: every published event gets a monotonic id and lands in a
|
||||
// fixed-capacity ring buffer so an SSE client that reconnects with
|
||||
// Last-Event-ID can be caught up without a full poll. The ring is
|
||||
// best-effort — when a client's resume point has already been overwritten,
|
||||
// Replay reports the gap and the caller falls back to the state snapshot
|
||||
// it sends on every connect anyway.
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
||||
)
|
||||
|
||||
// Stamped pairs an event with its bus-assigned monotonic id. IDs start at 1
|
||||
// and never repeat for the lifetime of the process.
|
||||
type Stamped struct {
|
||||
ID uint64
|
||||
Event *panelv1.Event
|
||||
}
|
||||
|
||||
// DefaultRingCap is how many recent events the bus retains for replay.
|
||||
// State/app/player events are small and logs dominate volume; at a burst of
|
||||
// ~200 events/s this still covers ~20s of reconnect window, and the SSE
|
||||
// snapshot covers anything older.
|
||||
const DefaultRingCap = 4096
|
||||
|
||||
type replayRing struct {
|
||||
mu sync.Mutex
|
||||
buf []Stamped
|
||||
next uint64 // id to assign to the NEXT appended event (starts at 1)
|
||||
head int // index of oldest valid entry
|
||||
n int // number of valid entries (<= cap)
|
||||
}
|
||||
|
||||
func newReplayRing(capacity int) *replayRing {
|
||||
if capacity <= 0 {
|
||||
capacity = DefaultRingCap
|
||||
}
|
||||
return &replayRing{buf: make([]Stamped, capacity), next: 1}
|
||||
}
|
||||
|
||||
// append stamps e with the next monotonic id, stores it, and returns the id.
|
||||
func (r *replayRing) append(e *panelv1.Event) uint64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
id := r.next
|
||||
r.next++
|
||||
idx := (r.head + r.n) % len(r.buf)
|
||||
if r.n < len(r.buf) {
|
||||
r.buf[idx] = Stamped{ID: id, Event: e}
|
||||
r.n++
|
||||
} else {
|
||||
r.buf[r.head] = Stamped{ID: id, Event: e}
|
||||
r.head = (r.head + 1) % len(r.buf)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// lastID returns the most recently assigned id (0 when nothing published).
|
||||
func (r *replayRing) lastID() uint64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.next - 1
|
||||
}
|
||||
|
||||
// since returns every retained event with ID > afterID, oldest first.
|
||||
// ok=false means the resume point has been overwritten (afterID is older
|
||||
// than the oldest retained event), so the returned slice — while still the
|
||||
// full retained suffix — has a gap before it; callers should resync from a
|
||||
// snapshot. afterID >= lastID returns (nil, true).
|
||||
func (r *replayRing) since(afterID uint64) (out []Stamped, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.n == 0 {
|
||||
// Nothing retained: only gapless if the caller is already current.
|
||||
return nil, afterID >= r.next-1
|
||||
}
|
||||
oldest := r.buf[r.head].ID
|
||||
ok = afterID >= oldest-1
|
||||
for i := 0; i < r.n; i++ {
|
||||
s := r.buf[(r.head+i)%len(r.buf)]
|
||||
if s.ID > afterID {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out, ok
|
||||
}
|
||||
Reference in New Issue
Block a user