Merge: drain the job queue that nothing was draining

FR-PLAT-AND-4's Rust half, and FR-PLAT-AND-3's resumability with it. The
queue's claim_next, complete, fail and recover_orphaned had no callers
outside their own tests, so the jobs table accumulated rows nothing ever
ran.

It also fixes a claim that was not safe across two connections: the
deferred transaction took a read lock for the SELECT and only tried to
upgrade at the UPDATE, so in WAL the second worker got
SQLITE_BUSY_SNAPSHOT, which a busy handler cannot retry away. It never
double-claimed, but the loser errored. Now one UPDATE ... RETURNING.

No handler is wired, deliberately. The only enqueue site reachable in
the shipping app produces remote thumbnail jobs already served by the
async grid worker, and inventing a second network path blind is not
worth a requirement reading as covered on the strength of plumbing.

Verified: clippy -D warnings clean, 376 dr-catalog and 548 dr-ui tests,
18 runner tests including four-thread contention and crash recovery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/traceability.md
#	ui/dr-ui/src/library.rs
#	ui/dr-ui/src/settings_ui.rs
This commit is contained in:
2026-08-29 23:49:38 +02:00
4 changed files with 1473 additions and 41 deletions
+23
View File
@@ -1936,6 +1936,29 @@ fn show_catalog_now(
}
};
// Before anything else can see this catalog, and exactly once per open —
// the early return above is what makes it once. The job queue is durable,
// so a run that was killed mid-job left its row marked `Running` with
// nobody holding it; recovery hands those back to be resumed rather than
// lost, and drops jobs naming photographs that have since been deleted.
//
// Here rather than wherever a runner starts, because there is no owner
// column in `jobs`: a second recovery pass while a worker held a claim
// would take that claim away from it.
match dr_catalog::runner::recover(cat.connection()) {
Ok(r) if r.did_anything() => log::info!(
"job queue: {} interrupted job(s) resumed, \
{} for deleted photographs dropped",
r.reclaimed,
r.reaped
),
Ok(_) => {}
// Not surfaced. The queue is rebuildable like everything else in the
// catalog, and a grid that refuses to open because a background queue
// could not be tidied is the worse failure by some way.
Err(e) => log::warn!("job queue could not be recovered: {e}"),
}
// The sidebar before the grid, because the grid's badges read collection
// membership — the same order the scan's completion uses.
crate::collections_ui::refresh_tree(window, coll_ctl, &cat);