platform
This commit is contained in:
1 parent
e706f7d907
commit
21bcb63440
42 files changed
+5903
No files matched your search
@@ -0,0 +1,201 @@
|
||||
// SnapshotHolder provides atomic hot-reload for SnapshotReader with drain
|
||||
// grace — solves the classic "Get concurrent with Close/munmap" race that
|
||||
// would otherwise produce uncatchable SIGBUS / SIGSEGV under reload.
|
||||
//
|
||||
// Pattern source: slask-finder's `RawItemsStore` and similar mmap+reload
|
||||
// stores, which delay unmap behind a configurable drain-grace timer so any
|
||||
// in-flight Get on the previously-current reader completes before the
|
||||
// underlying pages become unmapped. See docs/inventory-shape.md & the
|
||||
// finder WARP doc on mmap reload for the prior art.
|
||||
//
|
||||
// Use:
|
||||
//
|
||||
// holder := catalog.NewSnapshotHolder(30 * time.Second) // drain grace
|
||||
// if err := holder.Open("/var/lib/catalog/bygglagret.bin"); err != nil { ... }
|
||||
// // later, when a fresh snapshot lands:
|
||||
// if err := holder.Reload("/var/lib/catalog/bygglagret.bin"); err != nil { ... }
|
||||
// // pulls the projection; always hits the current reader:
|
||||
// p, ok := holder.Get(sku)
|
||||
//
|
||||
// holder.Get / Len are safe for parallel use. Open and Reload are also
|
||||
// safe for parallel use. Close stops the reaper goroutine and shuts the
|
||||
// current reader + drains down.
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultDrainGrace is the default drain period for SnapshotHolder. Finder's
|
||||
// analogous reload path uses ~30s; we mirror that value.
|
||||
const DefaultDrainGrace = 30 * time.Second
|
||||
|
||||
// SnapshotHolder swaps a SnapshotReader under load without unmapping pages
|
||||
// out from under in-flight Get()s. Set retains the previous reader behind a
|
||||
// drain timer; after the timer elapses the reaper goroutine closes the old
|
||||
// reader (and runs its munmap).
|
||||
type SnapshotHolder struct {
|
||||
current atomic.Pointer[SnapshotReader]
|
||||
|
||||
drainsMu sync.Mutex
|
||||
drains []*pendingClose
|
||||
drainGrace time.Duration
|
||||
|
||||
stopCh chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type pendingClose struct {
|
||||
r *SnapshotReader
|
||||
expireAt time.Time
|
||||
}
|
||||
|
||||
// NewSnapshotHolder returns a holder with the given drain grace (the time
|
||||
// the previous reader lives behind Set before being closed). Zero or
|
||||
// negative grace falls back to DefaultDrainGrace.
|
||||
func NewSnapshotHolder(grace time.Duration) *SnapshotHolder {
|
||||
if grace <= 0 {
|
||||
grace = DefaultDrainGrace
|
||||
}
|
||||
h := &SnapshotHolder{
|
||||
drainGrace: grace,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
go h.sweepLoop()
|
||||
return h
|
||||
}
|
||||
|
||||
// Open is shorthand for Set(ReadSnapshotFile(path)). Errors leave the
|
||||
// holder's current reader unchanged.
|
||||
func (h *SnapshotHolder) Open(path string) error {
|
||||
r, err := ReadSnapshotFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Set(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload atomically swaps the current reader for one freshly opened from
|
||||
// path. The old reader (if any) is queued for close after the drain grace
|
||||
// elapses; in-flight Get(s) on the old reader continue to work.
|
||||
func (h *SnapshotHolder) Reload(path string) error {
|
||||
r, err := ReadSnapshotFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Set(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set replaces the current reader with r. The previous reader is queued
|
||||
// for close after the drain grace elapses. r may be nil to clear.
|
||||
func (h *SnapshotHolder) Set(r *SnapshotReader) {
|
||||
if r == nil {
|
||||
h.current.Store(nil)
|
||||
return
|
||||
}
|
||||
old := h.current.Swap(r)
|
||||
if old == nil {
|
||||
return
|
||||
}
|
||||
h.drainsMu.Lock()
|
||||
h.drains = append(h.drains, &pendingClose{
|
||||
r: old,
|
||||
expireAt: time.Now().Add(h.drainGrace),
|
||||
})
|
||||
h.drainsMu.Unlock()
|
||||
}
|
||||
|
||||
// Get is the concurrent-safe shim over the current reader's Get. Returns
|
||||
// (zero, false) when the holder is empty.
|
||||
func (h *SnapshotHolder) Get(sku string) (Projection, bool) {
|
||||
r := h.current.Load()
|
||||
if r == nil {
|
||||
return Projection{}, false
|
||||
}
|
||||
return r.Get(sku)
|
||||
}
|
||||
|
||||
// Len returns the count of the current reader (0 if empty).
|
||||
func (h *SnapshotHolder) Len() int {
|
||||
r := h.current.Load()
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.Len()
|
||||
}
|
||||
|
||||
// Close stops the reaper goroutine and closes the current reader + any
|
||||
// pending drains without waiting for the grace period. Safe to call
|
||||
// multiple times.
|
||||
func (h *SnapshotHolder) Close() error {
|
||||
var firstErr error
|
||||
h.once.Do(func() {
|
||||
close(h.stopCh)
|
||||
r := h.current.Swap(nil)
|
||||
if r != nil {
|
||||
if err := r.Close(); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
h.drainsMu.Lock()
|
||||
drained := h.drains
|
||||
h.drains = nil
|
||||
h.drainsMu.Unlock()
|
||||
for _, d := range drained {
|
||||
if err := d.r.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// DrainCount returns the number of readers currently in the drain queue.
|
||||
// Useful for tests and operational metrics.
|
||||
func (h *SnapshotHolder) DrainCount() int {
|
||||
h.drainsMu.Lock()
|
||||
defer h.drainsMu.Unlock()
|
||||
return len(h.drains)
|
||||
}
|
||||
|
||||
// reapNow scans the drain queue and Closes any readers whose grace has
|
||||
// elapsed. Called by the sweeper goroutine on a tick.
|
||||
func (h *SnapshotHolder) reapNow() {
|
||||
now := time.Now()
|
||||
var alive []*pendingClose
|
||||
var reaped []*pendingClose
|
||||
h.drainsMu.Lock()
|
||||
for _, d := range h.drains {
|
||||
if !d.expireAt.After(now) {
|
||||
reaped = append(reaped, d)
|
||||
} else {
|
||||
alive = append(alive, d)
|
||||
}
|
||||
}
|
||||
h.drains = alive
|
||||
h.drainsMu.Unlock()
|
||||
for _, d := range reaped {
|
||||
_ = d.r.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SnapshotHolder) sweepLoop() {
|
||||
interval := h.drainGrace / 4
|
||||
if interval < 10*time.Millisecond {
|
||||
interval = 10 * time.Millisecond
|
||||
}
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-h.stopCh:
|
||||
return
|
||||
case <-t.C:
|
||||
h.reapNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user