// Package keyedmutex provides a lazily-populated per-key mutex, so a critical // section can be serialized per key (e.g. per workload id) without a global // lock. It is the shared form of the pattern that originated inline in the // GitOps sync handler; the deployer (per-workload deploy serialization) and the // volume-snapshot restore single-flight both use it. package keyedmutex import "sync" // Mutex hands out one *sync.Mutex per key on demand. The zero value is ready to // use. The internal map only grows (one entry per distinct key ever locked), // which is bounded in practice by the number of workloads. type Mutex struct { mu sync.Mutex m map[string]*sync.Mutex } func (k *Mutex) get(key string) *sync.Mutex { k.mu.Lock() defer k.mu.Unlock() if k.m == nil { k.m = make(map[string]*sync.Mutex) } mu, ok := k.m[key] if !ok { mu = &sync.Mutex{} k.m[key] = mu } return mu } // Lock blocks until the mutex for key is acquired, then returns its unlock func. func (k *Mutex) Lock(key string) func() { mu := k.get(key) mu.Lock() return mu.Unlock } // TryLock attempts to acquire the mutex for key without blocking. On success it // returns the unlock func and true; if the key is already locked it returns nil // and false so the caller can reject (e.g. HTTP 409) instead of queuing. func (k *Mutex) TryLock(key string) (func(), bool) { mu := k.get(key) if !mu.TryLock() { return nil, false } return mu.Unlock, true }