Skip to content

Prevent duplicate media progress from concurrent progress updates - #5640

Open
KeFan-J wants to merge 1 commit into
advplyr:masterfrom
KeFan-J:fix/media-progress-create-race
Open

KeFan-J wants to merge 1 commit into
advplyr:masterfrom
KeFan-J:fix/media-progress-create-race

Conversation

@KeFan-J

@KeFan-J KeFan-J commented Oct 7, 2026

Copy link
Copy Markdown

Brief summary

Concurrent progress updates for the same item can each create a mediaProgress row. This queues progress updates per user and media item so only one row is ever created.

Which issue is fixed?

Fixes #3845

May be related to #5627 and #5188.

In-depth Description

User.createUpdateMediaProgressFromPayload loads the existing progress for the item and creates a new row if there is none. The lookup and the insert are separate awaits, and mediaProgresses has no unique constraint on (userId, mediaItemId). Two requests for an item with no progress yet both miss the lookup, and both insert. As noted in #3845, this happens when a client calls /api/session/local and /api/me/progress/batch/update at the same time.

The duplicates stay until the next server start, when Database.cleanDatabase deletes the older ones. Until then, the in-memory user.mediaProgresses and the row a sync updates can be different rows.

This change wraps the existing logic in a small per-key promise queue (userId:episodeId|libraryItemId). Updates for the same item run one after another, so the second one finds the row created by the first and updates it. Updates for different items or users still run in parallel. A failed update doesn't block the ones queued after it, and the key is removed once its queue is empty.

I went with an in-process queue rather than a unique index because the server is a single process, and an index would need a migration that first removes duplicates on existing installs. Adding the index later is still possible on top of this.

How have you tested this?

  • New test/server/models/User.test.js:
    • Three concurrent updates for the same item create one row, and the cached user.mediaProgresses has one entry with the last value. On master this fails with 3 rows.
    • A failed update doesn't block the next queued update for the same item.
  • npm test: 360 passing.
  • Manual, local server with a 1-hour mp3:
    • Before: 2 concurrent PATCH /api/me/progress/:id for an item with no progress created duplicate rows in 29 of 30 rounds. 4 concurrent requests created 4 rows.
    • After: 0 duplicates in 30 rounds, and 4 concurrent requests left a single row.

🤖 Generated with Claude Code

createUpdateMediaProgressFromPayload looks up the existing mediaProgress
and creates one if none is found. Two requests for the same item arriving
together (e.g. /api/session/local and /api/me/progress/batch/update) both
miss the lookup and each create a row. Duplicates are only removed on the
next server start by cleanDatabase.

Queue progress updates per user and media item so each update sees the
row written by the previous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Saved Media Progress showing double entries

1 participant