package console import "sync" // keyedLocks is a refcounted mutex per string key: storelock.go's shape with // the key built by the caller. Entries drop at zero references, so an app // locked once does not hold a mutex for the life of the process. The zero // value is ready, which keeps &Server{} literals in tests safe. // // IN-PROCESS ONLY, like storeWalkLocks: temporary static sites spec §2 assumes // one console per deployment bucket, which is how every deployment runs. type keyedLocks struct { mu sync.Mutex m map[string]*keyedLock } type keyedLock struct { mu sync.Mutex refs int } // lock blocks until key's mutex is held and returns the release: // // defer s.lockApp(acct.Sqid, app)() func (l *keyedLocks) lock(key string) func() { l.mu.Lock() if l.m == nil { l.m = make(map[string]*keyedLock) } e := l.m[key] if e == nil { e = &keyedLock{} l.m[key] = e } e.refs++ l.mu.Unlock() e.mu.Lock() return func() { e.mu.Unlock() l.mu.Lock() e.refs-- if e.refs == 0 { delete(l.m, key) } l.mu.Unlock() } } // lockAccount serialises counting temporary apps against creating or // restoring one, which is what makes the cap exact (spec §3, §6). // // LOCK ORDER: account before app, never the other way round. Only // /new-temporary and restoreApp take this lock. func (s *Server) lockAccount(sqid string) func() { return s.acctLocks.lock(sqid) } // lockApp is the per-app mutex every lifecycle transition takes: keep, ttl, // restore, trash, the purge door and each step of the expiry pass (spec §5). func (s *Server) lockApp(sqid, app string) func() { return s.appLocks.lock(sqid + "/" + app) }