mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
|
)
|
|
|
|
// CandidateCache is intentionally rebuildable. A real Redis implementation
|
|
// can satisfy this interface, but no cache operation is an ownership fence.
|
|
type CandidateCache struct {
|
|
mu sync.RWMutex
|
|
candidates map[string]domain.Candidate
|
|
}
|
|
|
|
func NewCandidateCache() *CandidateCache {
|
|
return &CandidateCache{candidates: make(map[string]domain.Candidate)}
|
|
}
|
|
|
|
// RebuildFromQueue is the safe restart/failover path for the cache. Queue
|
|
// expiry and state filtering happen at the authoritative source before the
|
|
// cache is atomically replaced; callers never have to reconstruct those
|
|
// rules from a stale Redis index.
|
|
func RebuildFromQueue(cache *CandidateCache, queue *domain.Queue, now time.Time) error {
|
|
if cache == nil || queue == nil || now.IsZero() {
|
|
return fmt.Errorf("invalid candidate rebuild arguments")
|
|
}
|
|
return cache.Rebuild(queue.Candidates(now))
|
|
}
|
|
|
|
func (c *CandidateCache) Upsert(candidate domain.Candidate) error {
|
|
if candidate.TicketID == "" || candidate.PlayerID == "" || candidate.EnqueuedAt.IsZero() {
|
|
return fmt.Errorf("invalid candidate")
|
|
}
|
|
c.mu.Lock()
|
|
c.candidates[candidate.TicketID] = candidate
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (c *CandidateCache) Remove(ticketID string) {
|
|
c.mu.Lock()
|
|
delete(c.candidates, ticketID)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *CandidateCache) Snapshot(now time.Time) []domain.Candidate {
|
|
c.mu.RLock()
|
|
result := make([]domain.Candidate, 0, len(c.candidates))
|
|
for _, candidate := range c.candidates {
|
|
if !candidate.EnqueuedAt.After(now) {
|
|
result = append(result, candidate)
|
|
}
|
|
}
|
|
c.mu.RUnlock()
|
|
sort.Slice(result, func(i, j int) bool {
|
|
if !result[i].EnqueuedAt.Equal(result[j].EnqueuedAt) {
|
|
return result[i].EnqueuedAt.Before(result[j].EnqueuedAt)
|
|
}
|
|
return result[i].TicketID < result[j].TicketID
|
|
})
|
|
return result
|
|
}
|
|
|
|
// Rebuild replaces the cache atomically with the authoritative queue view.
|
|
// Callers should invoke this after Redis restart, failover, or a cache miss;
|
|
// the supplied candidates must already have passed durable queue checks.
|
|
func (c *CandidateCache) Rebuild(candidates []domain.Candidate) error {
|
|
rebuilt := make(map[string]domain.Candidate, len(candidates))
|
|
for _, candidate := range candidates {
|
|
if candidate.TicketID == "" || candidate.PlayerID == "" || candidate.EnqueuedAt.IsZero() {
|
|
return fmt.Errorf("invalid candidate in rebuild")
|
|
}
|
|
if _, exists := rebuilt[candidate.TicketID]; exists {
|
|
return fmt.Errorf("duplicate candidate in rebuild")
|
|
}
|
|
rebuilt[candidate.TicketID] = candidate
|
|
}
|
|
c.mu.Lock()
|
|
c.candidates = rebuilt
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|