package web import ( "hash/fnv" "sync" ) // The striped table lock, and why it is only ever an optimisation. // // The database's version column is the real concurrency authority: every write // to a table is conditional on the version the writer read, so two writers that // race produce one winner and one ErrStaleTable no matter what happens in // memory. This lock exists purely to make the loser lose *before* it does the // work, rather than after — it serialises the read-modify-write on a table so the // common case doesn't burn an engine step and a marshal only to be told it was // stale. // // It is a fixed array hashed on table id, never a map you can delete from, and // that is deliberate. A map of mutexes keyed by table id, cleaned up when a table // empties, will hand two goroutines two different mutex objects for the same // table across a delete-and-recreate — which is no lock at all. A fixed array has // no lifecycle: the same id always hashes to the same mutex, forever. The only // cost is that two unrelated tables can collide onto one stripe and briefly wait // on each other, which is harmless. // // A redeploy is the case that proves the version column has to be the authority: // during a drain two processes are running, each with its own array, so a table // is "locked" by two mutexes that know nothing about each other. The version // column is the only thing both processes share, and it is what keeps them // correct while the mutexes are useless. // lockStripes is how many mutexes the array holds. A power of two so the mask is // clean; large enough that collisions between live tables are rare. const lockStripes = 256 type stripedLocks struct { m [lockStripes]sync.Mutex } func newStripedLocks() *stripedLocks { return &stripedLocks{} } // forTable returns the mutex a given table hashes onto. The same id always // returns the same mutex. func (s *stripedLocks) forTable(id string) *sync.Mutex { h := fnv.New32a() _, _ = h.Write([]byte(id)) return &s.m[h.Sum32()&(lockStripes-1)] } // withTable runs fn while holding the table's stripe. The lock is released when // fn returns — it never spans a network read or an SSE send, only the // read-modify-write against the database. func (s *stripedLocks) withTable(id string, fn func() error) error { mu := s.forTable(id) mu.Lock() defer mu.Unlock() return fn() }