Files
2026-08-19 18:08:00 +12:00

62 lines
2.4 KiB
Go

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
}