Skip to content

[Bug] Push-echo tokens are armed at job start but expire after 10 min — long pushes leak their later echoes as real plays #372

Description

@foux

Summary

The push-echo tokens (_recently_pushed_watched, _PUSHED_WATCHED_TTL = 10 min) are armed once, for every item, at the very start of a full push — but consumed only when each item's mark_watched echo comes back, which on a long push can be 10-20 minutes later. Every echo arriving after the TTL is treated as a genuine play: a phantom WatchEvent stamped at push time, plus a scrobble stop forwarded to Trakt/Simkl/MDBList (#369).

What happened (v2.11.0, Jellyfin, ~16 400 items, push duration 21 min)

Full push started 09:06:52 UTC. Two groups of items were legitimately marked on the server later in the run:

  • 50 episodes marked at 09:21 (14 min after start) → 50 phantom WatchEvents (provisional=true, progress_seconds=0, progress_percent=1) + 27 spurious Trakt plays
  • 11 episodes marked at 09:26 (19 min after start) → 11 phantom WatchEvents + 11 spurious Trakt plays

Items marked in the first 10 minutes of the same push were swallowed correctly. Scrob history and Trakt cleaned by hand.

The next day's push on v2.13.0 (with #362 batching, 9 min total) produced zero phantom rows — only because every mark happened to land inside the window. Anyone with a large library or a slow server is still exposed.

Why

routers/sync.py, full push, before the push loop even starts:

if conn.push_watched:
    for mid in watched_ids:
        for sid in source_ids_map.get(mid, []):
            if echoes_watched:
                mark_pushed_watched(user_id, mid)          # armed now…
                watched_sid_to_mids.setdefault(sid, set()).add(mid)

…while _consume_recently_pushed_watched() in routers/webhooks.py compares the echo's arrival time against that arming timestamp:

pushed_at = queue.pop(0)
return (datetime.utcnow() - pushed_at) < _PUSHED_WATCHED_TTL   # 10 min

The comment above _recently_pushed_watched says "Set right before each outbound mark-watched call", which is what the TTL was designed around — the up-front arming (introduced with the sid-grouping for #298) silently broke that assumption. Note that a stale token is also popped on expiry, so the item's real echo is not merely unprotected: the token is gone for good.

Suggested fix

Arm the token inside _push_watched_group / _push_known / _push_lookup, immediately before the mark_watched call (for every mid of the group), instead of in the pre-loop. Then the 10-minute TTL only has to cover webhook delivery latency, which is what it was meant for. Alternatively (belt and braces), don't expire tokens by wall-clock at all while the push job that armed them is still running.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions