62 lines
2.4 KiB
Go
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
|
||
|
|
}
|