fix(playlists): every sync path locks the list before its items, and holds it to the write

The mirror refresh re-ingested inside its row lock, and that ingest's commit let go of the lock
before the replace: a remove made then was undone and the list read as synced. Push, re-pull and a
delete touched items before the list row, a deadlock beside the playlist-first route edits. A push
now re-plans from the items under the lock and derives dirty after its YouTube writes.
lock_playlists moves to the sync package. Tests pin both ids of a move's lock and the claim's
own connection.
This commit is contained in:
2026-09-27 21:56:23 +02:00
parent 2abd4ffcb6
commit 26697472cf
6 changed files with 329 additions and 47 deletions
+8 -2
View File
@@ -183,8 +183,10 @@ PLEX_ITEMS = PlexPlaylistItem.__table__.name
f"/api/playlists/{pid}/items/v1/move", json={"target_playlist_id": other}
),
lambda c, pid, other: c.put(f"/api/playlists/{pid}/order", json={"video_ids": ["v1"]}),
# the ORM cascade deletes the items before the list row
lambda c, pid, other: c.delete(f"/api/playlists/{pid}"),
],
ids=["add-present", "add-at", "remove", "move", "reorder"],
ids=["add-present", "add-at", "remove", "move", "reorder", "delete"],
)
def test_a_youtube_item_write_locks_the_playlist_before_any_item(
db, make_user, as_user, statements, call
@@ -208,7 +210,11 @@ def test_a_move_locks_both_lists_in_one_ordered_statement(db, make_user, as_user
)
assert r.status_code == 200, r.text
lock = _locked_before_items(statements, YT, YT_ITEMS)
assert lock and " IN (" in lock and "ORDER BY" in lock, lock
assert lock and "ORDER BY" in lock, lock
# BOTH lists in that one statement: one bound id per list in its IN (…) (a single-id lock
# renders the same IN and ORDER BY)
listed = re.search(r" IN \((.*?)\) ORDER BY", lock)
assert listed and len(re.findall(r"%\(\w+\)s", listed.group(1))) == 2, lock
def test_watch_later_add_and_remove_lock_it_before_any_item(db, make_user, as_user, statements):