package api import ( "context" "sync" "time" "github.com/ponzischeme89/memby/server/internal/store" ) // featurePolicyTTL is how stale the cached feature policy may be. // // The document is read on the request path from sixteen places and by /v1/status, which // every open television polls every ten seconds — so on a four-set household the policy // was fetched from Postgres somewhere over twenty times a minute to answer a question // whose answer changes when an operator presses a switch. Each read is a millisecond and // none of them was ever the reason a screen was slow; what they were is a standing draw // on a connection pool that the requests which *are* slow have to queue behind. // // Five seconds because the operator is the only writer and their own write clears this // instance's copy outright: the window is not "how long until my change takes effect" but // "how long until an instance that did not make the change notices", which is the same // question WatchMaintenance answers with thirty. const featurePolicyTTL = 5 * time.Second // featurePolicyCache is a read-through cache with a deliberately simple concurrency // story: a stale read takes the lock, refreshes, and every other caller waits for that // one refresh rather than starting its own. That is worth stating because the opposite — // releasing the lock to query — is what turns one expired entry into a thundering herd of // identical queries, which is the failure this exists to prevent. type featurePolicyCache struct { mu sync.Mutex value store.FeaturePolicy valid bool fetched time.Time } // read returns the cached value, refreshing through load when it has expired. load is // only ever called with the lock held, so it must not itself read the cache. func (c *featurePolicyCache) read( ctx context.Context, load func(context.Context) store.FeaturePolicy, ) store.FeaturePolicy { c.mu.Lock() defer c.mu.Unlock() if c.valid && time.Since(c.fetched) < featurePolicyTTL { return c.value } c.value = load(ctx) c.valid = true c.fetched = time.Now() return c.value } // invalidate drops the copy so the next read goes to Postgres. Called by the operator's // own write: a console that showed the old answer back to the person who had just // changed it would read as a save that failed. func (c *featurePolicyCache) invalidate() { c.mu.Lock() defer c.mu.Unlock() c.valid = false }