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()
}
