0.2.79 - Slow api fixes
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user