55 lines
2.1 KiB
Go
55 lines
2.1 KiB
Go
package api
|
|
|
|
import "sync"
|
|
|
|
// consideredFollows bounds the memory below. A household has a handful of episodes in
|
|
// flight; this is generous enough that a whole evening's viewing fits and small enough
|
|
// that nothing a client can do grows it into a leak.
|
|
const consideredFollows = 512
|
|
|
|
// followChecks remembers which (viewer, episode) pairs have already been put through
|
|
// the automatic My Shows check.
|
|
//
|
|
// The check is hung off playback reports, and a report arrives every ten seconds for the
|
|
// length of an episode. Its own guard — half watched — is true for every one of those
|
|
// after the halfway point, so on a forty-minute episode the second half of the viewing
|
|
// ran the whole check a hundred times: two serial Emby lookups, the Sonarr catalogue and
|
|
// a Postgres write, all of it on the response path of a request the television makes
|
|
// while somebody is watching something. Every run after the first reached the same
|
|
// `inserted == false` and discarded its own work.
|
|
//
|
|
// Deduplicating on the durable insert is correct and was never the problem; what it
|
|
// could not do is prevent the work leading up to it. This is deliberately in memory and
|
|
// deliberately lossy, the playbackTitles arrangement: a restarted gateway does the check
|
|
// once more per episode, which is the cost of one lookup rather than a schema change.
|
|
type followChecks struct {
|
|
mu sync.Mutex
|
|
seen map[string]struct{}
|
|
order []string
|
|
}
|
|
|
|
// claim reports whether this is the first time the pair has been offered, and records it.
|
|
// One call does both because two would be a race between the check and the record, and
|
|
// the thing being protected is precisely a burst of concurrent reports.
|
|
func (f *followChecks) claim(userID, itemID string) bool {
|
|
if userID == "" || itemID == "" {
|
|
return false
|
|
}
|
|
key := userID + "|" + itemID
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.seen == nil {
|
|
f.seen = make(map[string]struct{}, consideredFollows)
|
|
}
|
|
if _, known := f.seen[key]; known {
|
|
return false
|
|
}
|
|
f.seen[key] = struct{}{}
|
|
f.order = append(f.order, key)
|
|
if len(f.order) > consideredFollows {
|
|
delete(f.seen, f.order[0])
|
|
f.order = f.order[1:]
|
|
}
|
|
return true
|
|
}
|