Files
memby/server/internal/api/playback_follow.go
T

55 lines
2.1 KiB
Go
Raw Normal View History

2026-08-19 18:08:00 +12:00
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
}