Fix queue claim concurrency Refs #6
This commit is contained in:
@@ -48,7 +48,7 @@ def test_manual_queue_items_are_persisted_and_imported(tmp_path, monkeypatch):
|
||||
queued = main.state.list_queue_items()
|
||||
assert len(queued) == 1
|
||||
assert queued[0]["source_type"] == "manual"
|
||||
assert queued[0]["state"] == "manual_batch"
|
||||
assert queued[0]["state"] == "ready"
|
||||
|
||||
assert main._import_manual_batches(main.Importer(movies, tv)) == 1
|
||||
assert main.state.list_queue_items() == []
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import threading
|
||||
|
||||
from importarr.config import Settings
|
||||
from importarr.state import State
|
||||
|
||||
@@ -259,6 +261,97 @@ def test_run_now_conflicts_when_item_is_claimed(tmp_path, monkeypatch):
|
||||
raise AssertionError("expected HTTPException")
|
||||
|
||||
|
||||
def test_bulk_run_now_skips_item_claimed_by_worker(tmp_path, monkeypatch):
|
||||
main, download, movies, tv = configure_main(tmp_path, monkeypatch)
|
||||
batch = download / "Release"
|
||||
batch.mkdir(parents=True)
|
||||
source = batch / "A.mkv"
|
||||
source.write_bytes(b"a")
|
||||
created_batch = main.state.add_manual_batch(batch)
|
||||
row = main.state.upsert_queue_item(source_type="manual", source_id=str(source), source_path=source, name=source.name, state="ready", batch_id=created_batch["id"])
|
||||
main.state.claim_queue_item(row["id"], "worker", {"ready"})
|
||||
|
||||
assert main._import_manual_batches(main.Importer(movies, tv)) == 0
|
||||
assert source.exists()
|
||||
assert main.state.get_queue_item(row["id"])["claimed_by"] == "worker"
|
||||
|
||||
|
||||
def test_retry_loses_atomic_race_with_worker_claim(tmp_path, monkeypatch):
|
||||
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
|
||||
row = main.state.upsert_queue_item(source_type="manual", source_id="a", name="A.mkv", state="failed")
|
||||
original = main.state.transition_queue_item_if_unclaimed
|
||||
|
||||
def claim_then_transition(*args, **kwargs):
|
||||
main.state.claim_queue_item(row["id"], "worker", {"failed"})
|
||||
return original(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(main.state, "transition_queue_item_if_unclaimed", claim_then_transition)
|
||||
|
||||
try:
|
||||
main.queue_item_action(row["id"], main.QueueItemActionRequest(action="retry"))
|
||||
except main.HTTPException as exc:
|
||||
assert exc.status_code == 409
|
||||
else:
|
||||
raise AssertionError("expected HTTPException")
|
||||
assert main.state.get_queue_item(row["id"])["claimed_by"] == "worker"
|
||||
|
||||
|
||||
def test_ignore_loses_atomic_race_with_worker_claim(tmp_path, monkeypatch):
|
||||
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
|
||||
row = main.state.upsert_queue_item(source_type="manual", source_id="a", name="A.mkv", state="ready")
|
||||
original = main.state.transition_queue_item_if_unclaimed
|
||||
|
||||
def claim_then_transition(*args, **kwargs):
|
||||
main.state.claim_queue_item(row["id"], "worker", {"ready"})
|
||||
return original(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(main.state, "transition_queue_item_if_unclaimed", claim_then_transition)
|
||||
|
||||
try:
|
||||
main.queue_item_action(row["id"], main.QueueItemActionRequest(action="ignore"))
|
||||
except main.HTTPException as exc:
|
||||
assert exc.status_code == 409
|
||||
else:
|
||||
raise AssertionError("expected HTTPException")
|
||||
assert main.state.get_queue_item(row["id"])["claimed_by"] == "worker"
|
||||
|
||||
|
||||
def test_shutdown_waits_for_active_import_before_releasing_claims(tmp_path, monkeypatch):
|
||||
main, download, _movies, _tv = configure_main(tmp_path, monkeypatch)
|
||||
source = download / "A.mkv"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_bytes(b"a")
|
||||
row = main.state.upsert_queue_item(source_type="manual", source_id=str(source), source_path=source, name=source.name, state="ready")
|
||||
started = threading.Event()
|
||||
finish = threading.Event()
|
||||
|
||||
class BlockingImporter:
|
||||
def import_file(self, *args, **kwargs):
|
||||
started.set()
|
||||
finish.wait()
|
||||
raise RuntimeError("stopped")
|
||||
|
||||
def active_worker():
|
||||
claimed = main.state.claim_queue_item(row["id"], main.WORKER_ID, {"ready"})
|
||||
main._import_queue_item(claimed, BlockingImporter(), from_worker=True)
|
||||
|
||||
worker = threading.Thread(target=active_worker)
|
||||
monkeypatch.setattr(main, "_worker_thread", worker)
|
||||
worker.start()
|
||||
assert started.wait(1)
|
||||
shutdown = threading.Thread(target=main.shutdown_queue_worker)
|
||||
shutdown.start()
|
||||
|
||||
shutdown.join(0.05)
|
||||
assert shutdown.is_alive()
|
||||
assert main.state.get_queue_item(row["id"])["claimed_by"] == main.WORKER_ID
|
||||
|
||||
finish.set()
|
||||
shutdown.join(1)
|
||||
assert not shutdown.is_alive()
|
||||
assert main.state.get_queue_item(row["id"])["claimed_by"] is None
|
||||
|
||||
|
||||
def test_remove_conflicts_when_item_is_claimed(tmp_path, monkeypatch):
|
||||
main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch)
|
||||
row = main.state.upsert_queue_item(source_type="manual", source_id="a", name="A.mkv", state="ready")
|
||||
|
||||
Reference in New Issue
Block a user