Recover unfinished scans so their items aren't stranded
get_session_items / get_open_items / latest_scan_id all require finished_at IS NOT NULL, but the M365 and Google engines return early on abort (skipping finish_scan) and a process kill mid-scan (deploy, OOM, crash) never reaches it either. Result on prod: 41/42 scans had finished_at NULL, so 291 already-saved flagged items were invisible — the grid showed nothing. - finalize_orphan_scans(): finalises every finished_at-NULL scan; runs once at startup before the scheduler (nothing is scanning at boot, so any unfinished scan is dead). Recovers existing stranded items and guards against future mid-scan restarts. - run_scan: finalise the DB scan on the abort early-return too, so a stopped scan's items stay visible without waiting for a restart. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bf589bf7a
commit
29d9168643
5 files changed
+100
No files matched your search
@@ -265,3 +265,51 @@ class TestExportImport:
|
||||
tgt.import_db(str(export_path), mode="replace")
|
||||
results = tgt.lookup_data_subject("290472-1234")
|
||||
assert len(results) >= 1
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Orphan-scan recovery (crash / kill / mid-scan restart)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestOrphanScanRecovery:
|
||||
|
||||
def _start_unfinished_scan(self, db, item_id):
|
||||
"""Begin a scan and save an item but never call finish_scan."""
|
||||
sid = db.begin_scan({"sources": ["email"], "user_ids": []})
|
||||
db.save_item(sid, _make_card(item_id=item_id))
|
||||
return sid
|
||||
|
||||
def test_unfinished_scan_items_hidden_until_recovery(self, tmp_db):
|
||||
self._start_unfinished_scan(tmp_db, "orphan-1")
|
||||
# Not finalised → invisible to the open-items view
|
||||
assert tmp_db.get_open_items() == []
|
||||
|
||||
def test_recovery_finalises_and_reveals_items(self, tmp_db):
|
||||
self._start_unfinished_scan(tmp_db, "orphan-1")
|
||||
self._start_unfinished_scan(tmp_db, "orphan-2")
|
||||
|
||||
recovered = tmp_db.finalize_orphan_scans()
|
||||
assert recovered == 2
|
||||
|
||||
ids = {row["id"] for row in tmp_db.get_open_items()}
|
||||
assert ids == {"orphan-1", "orphan-2"}
|
||||
|
||||
def test_recovery_leaves_finished_scans_untouched(self, tmp_db):
|
||||
sid = tmp_db.begin_scan({"sources": ["email"], "user_ids": []})
|
||||
tmp_db.save_item(sid, _make_card(item_id="done-1"))
|
||||
tmp_db.finish_scan(sid, total_scanned=1)
|
||||
before = tmp_db._connect().execute(
|
||||
"SELECT finished_at FROM scans WHERE id=?", (sid,)
|
||||
).fetchone()[0]
|
||||
|
||||
assert tmp_db.finalize_orphan_scans() == 0 # nothing to recover
|
||||
|
||||
after = tmp_db._connect().execute(
|
||||
"SELECT finished_at FROM scans WHERE id=?", (sid,)
|
||||
).fetchone()[0]
|
||||
assert after == before # finished_at not rewritten
|
||||
|
||||
def test_recovery_is_idempotent(self, tmp_db):
|
||||
self._start_unfinished_scan(tmp_db, "orphan-1")
|
||||
assert tmp_db.finalize_orphan_scans() == 1
|
||||
assert tmp_db.finalize_orphan_scans() == 0
|
||||
Reference in new issue
Block a user