package spindle
import (
"sync"
"sync/atomic"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
)
const lockStripes = 8192
// every did hashes to one of these mutexes, and dids sharing one is fine as long as holders only touch the db
type stripedLocks [lockStripes]sync.Mutex
func fnv32a(s string) uint32 {
h := uint32(2166136261)
for i := 0; i < len(s); i++ {
h ^= uint32(s[i])
h *= 16777619
}
return h
}
func stripeOf(did syntax.DID) uint32 {
return fnv32a(string(did)) % lockStripes
}
type heldLock struct{ mu *sync.Mutex }
func (h heldLock) Unlock() { h.mu.Unlock() }
func (t *stripedLocks) lock(did syntax.DID) heldLock {
mu := &t[stripeOf(did)]
mu.Lock()
return heldLock{mu}
}
// don't hold this over a knot or git call, other dids on the stripe wait too
func (s *Spindle) lockRepo(repoDid syntax.DID) heldLock {
return s.repoLocks.lock(repoDid)
}
// seeded from the newest turn in case the clock went back across a restart
type turnClock struct {
once sync.Once
last atomic.Int64
}
// a turn is one refresh of a repo, numbered when it starts. its knot read can go stale,
// so the write only lands if no newer turn wrote first
type repoTurn struct {
s *Spindle
did syntax.DID
seq int64
}
func (s *Spindle) beginTurn(did syntax.DID) *repoTurn {
s.turns.once.Do(func() {
newest, err := s.db.NewestRepoTurn()
if err != nil {
s.l.Error("reading the newest repo turn, falling back to the clock", "err", err)
}
s.turns.last.Store(newest)
})
for {
last := s.turns.last.Load()
seq := max(time.Now().UnixNano(), last+1)
if s.turns.last.CompareAndSwap(last, seq) {
return &repoTurn{s: s, did: did, seq: seq}
}
}
}
// applies even past a later turn, otherwise an older refresh could undo a wipe
func (r *repoTurn) force(fn func() error) error {
defer r.s.lockRepo(r.did).Unlock()
if _, err := r.s.db.AdvanceRepoTurn(r.did, r.seq, time.Now()); err != nil {
return err
}
return fn()
}
func (r *repoTurn) apply(fn func() error) (applied bool, err error) {
defer r.s.lockRepo(r.did).Unlock()
applied, err = r.s.db.AdvanceRepoTurn(r.did, r.seq, time.Now())
if err != nil || !applied {
return false, err
}
return true, fn()
}