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 }