The post-scan reconcile previously ran AFTER the worker's 'finished'
signal, which flips db_update_state status to 'finished'. Automations
wait for a scan by polling that status, so they stopped waiting before
the reconcile ran — and the dashboard/Tools card showed "Completed" then
flipped to "Reading file tags…". For incremental scans this was
invisible (sub-second); for a full refresh it was a real gap (a chained
automation would fire minutes before the IDs were filled).
Fix: the worker now routes completion through _emit_finished(), which
runs self.post_scan_hook (the reconcile) FIRST, then emits 'finished'.
The hook is injected by the web layer (it owns path resolution). So:
- status stays 'running' through the reconcile,
- the reconcile pushes its phase ("Reading file tags for N new tracks…")
and per-track progress through the SAME db_update_state callbacks the
scan already uses — so automations, the dashboard card, and the Tools
page all see it for free and wait for it,
- 'finished' is emitted exactly once, AFTER the reconcile — race-free, no
status blip a poll could catch,
- best-effort: a hook exception never blocks 'finished', so a scan can't
get stranded as perpetually 'running'.
Both scan entry points (_run_database_update_task, _run_deep_scan_task)
set the hook before run()/run_deep_scan(); the redundant post-run calls
are removed.
5 ordering tests pin the contract (hook-before-finished, finished still
fires without a hook, hook exception doesn't block finished, hook gets
the worker). Full suite clean (only pre-existing soundcloud /app env
failures remain).
66 lines
2.4 KiB
Python
66 lines
2.4 KiB
Python
"""The auto-reconcile must run as the FINAL scan phase — inside the worker's
|
|
completion, BEFORE the 'finished' signal — so the scan's status stays
|
|
'running' through it. That ordering is what makes automations (which poll for
|
|
completion), the dashboard card, and the Tools page all treat the reconcile as
|
|
part of the scan and wait for it, rather than seeing 'finished' early and
|
|
missing the tail. These pin that contract on DatabaseUpdateWorker._emit_finished.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.database_update_worker import DatabaseUpdateWorker
|
|
|
|
|
|
def _bare_worker():
|
|
# __new__ avoids the full media-client/config init; _emit_finished only
|
|
# touches self.callbacks + self.post_scan_hook.
|
|
w = DatabaseUpdateWorker.__new__(DatabaseUpdateWorker)
|
|
w.callbacks = {'finished': [], 'error': [], 'progress_updated': [],
|
|
'phase_changed': [], 'artist_processed': []}
|
|
w.post_scan_hook = None
|
|
return w
|
|
|
|
|
|
def test_post_scan_hook_runs_before_finished():
|
|
w = _bare_worker()
|
|
order = []
|
|
w.post_scan_hook = lambda worker: order.append('hook')
|
|
w.callbacks['finished'].append(lambda *a: order.append('finished'))
|
|
w._emit_finished(1, 2, 3, 4, 5)
|
|
assert order == ['hook', 'finished'] # reconcile happens inside the running window
|
|
|
|
|
|
def test_finished_receives_original_args():
|
|
w = _bare_worker()
|
|
got = []
|
|
w.callbacks['finished'].append(lambda *a: got.append(a))
|
|
w._emit_finished(1, 2, 3, 4, 5)
|
|
assert got == [(1, 2, 3, 4, 5)]
|
|
|
|
|
|
def test_no_hook_still_emits_finished():
|
|
# Backward-compatible: a worker with no hook signals finished exactly as before.
|
|
w = _bare_worker()
|
|
got = []
|
|
w.callbacks['finished'].append(lambda *a: got.append(a))
|
|
w._emit_finished(0, 0, 0, 0, 0)
|
|
assert got == [(0, 0, 0, 0, 0)]
|
|
|
|
|
|
def test_hook_exception_never_blocks_finished():
|
|
# A reconcile failure must not strand the scan as perpetually 'running'.
|
|
w = _bare_worker()
|
|
fired = []
|
|
w.post_scan_hook = lambda worker: (_ for _ in ()).throw(RuntimeError("boom"))
|
|
w.callbacks['finished'].append(lambda *a: fired.append(a))
|
|
w._emit_finished(1, 1, 1, 1, 1)
|
|
assert fired == [(1, 1, 1, 1, 1)]
|
|
|
|
|
|
def test_hook_receives_the_worker():
|
|
w = _bare_worker()
|
|
seen = []
|
|
w.post_scan_hook = lambda worker: seen.append(worker)
|
|
w.callbacks['finished'].append(lambda *a: None)
|
|
w._emit_finished(0, 0, 0, 0, 0)
|
|
assert seen == [w]
|