Two subsystems post-process the same completed transfer: the browser-poll
status endpoint (web_server) and the background download monitor. Both watch
the same slskd/streaming transfers and each launches the verification
pipeline. When one path quarantines + requeues the next-best candidate
(clearing username/filename, status -> 'searching'), the monitor's
already-submitted run_post_processing_worker then runs, finds no source info,
and falsely marks the task 'failed' ("missing file or source information") —
clobbering the in-flight retry while a parallel attempt imports the song.
Fix: a single atomic claim (downloading/queued -> post_processing under
tasks_lock) so exactly one path processes each download.
- runtime_state: new claim_for_post_processing() helper
- post_processing: race guard — worker bails (no fail/notify) if the task is
no longer 'post_processing' when it runs
- web_server: both poll paths (Soulseek + streaming) claim before launching;
claim is released on thread-launch failure
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
2.7 KiB
Python
63 lines
2.7 KiB
Python
import pytest
|
|
|
|
import core.runtime_state as runtime_state
|
|
|
|
|
|
def test_mark_task_completed_requires_tasks_lock():
|
|
original_tasks = dict(runtime_state.download_tasks)
|
|
runtime_state.download_tasks.clear()
|
|
runtime_state.download_tasks["task-1"] = {"status": "running", "stream_processed": False}
|
|
|
|
try:
|
|
with pytest.raises(RuntimeError, match="tasks_lock"):
|
|
runtime_state.mark_task_completed("task-1")
|
|
finally:
|
|
runtime_state.download_tasks.clear()
|
|
runtime_state.download_tasks.update(original_tasks)
|
|
|
|
|
|
def test_mark_task_completed_succeeds_when_lock_held():
|
|
original_tasks = dict(runtime_state.download_tasks)
|
|
runtime_state.download_tasks.clear()
|
|
runtime_state.download_tasks["task-1"] = {"status": "running", "stream_processed": False}
|
|
|
|
try:
|
|
with runtime_state.tasks_lock:
|
|
assert runtime_state.mark_task_completed("task-1", {"name": "Song One"}) is True
|
|
assert runtime_state.download_tasks["task-1"]["status"] == "completed"
|
|
assert runtime_state.download_tasks["task-1"]["stream_processed"] is True
|
|
assert runtime_state.download_tasks["task-1"]["track_info"] == {"name": "Song One"}
|
|
finally:
|
|
runtime_state.download_tasks.clear()
|
|
runtime_state.download_tasks.update(original_tasks)
|
|
|
|
|
|
@pytest.fixture
|
|
def _isolated_tasks():
|
|
original_tasks = dict(runtime_state.download_tasks)
|
|
runtime_state.download_tasks.clear()
|
|
yield runtime_state.download_tasks
|
|
runtime_state.download_tasks.clear()
|
|
runtime_state.download_tasks.update(original_tasks)
|
|
|
|
|
|
@pytest.mark.parametrize("start_status", ["downloading", "queued"])
|
|
def test_claim_for_post_processing_wins_from_active_status(_isolated_tasks, start_status):
|
|
_isolated_tasks["t1"] = {"status": start_status}
|
|
assert runtime_state.claim_for_post_processing("t1") is True
|
|
assert _isolated_tasks["t1"]["status"] == "post_processing"
|
|
|
|
|
|
@pytest.mark.parametrize("start_status", ["post_processing", "searching", "completed", "failed", "cancelled"])
|
|
def test_claim_for_post_processing_loses_when_already_owned(_isolated_tasks, start_status):
|
|
"""A task already being post-processed by the monitor (post_processing),
|
|
requeued by a quarantine retry (searching), or already terminal must NOT be
|
|
re-claimed — that is the double-processing race that produced the bogus
|
|
'missing file or source information' failure."""
|
|
_isolated_tasks["t1"] = {"status": start_status}
|
|
assert runtime_state.claim_for_post_processing("t1") is False
|
|
assert _isolated_tasks["t1"]["status"] == start_status # untouched
|
|
|
|
|
|
def test_claim_for_post_processing_missing_task_returns_false(_isolated_tasks):
|
|
assert runtime_state.claim_for_post_processing("absent") is False
|