fix(analysis): emit Cancelled event + assert lock-release after cancel (0.87.13)

Two 0.87.4 follow-ups:

1. Cron tick: existing shutdown-on-cancel test now also asserts the
   advisory lock is re-acquirable after cancel-during-tick. Without
   this, a refactor moving `pg_advisory_unlock` under the cancel arm
   would ship green and break multi-replica safety.

2. Analysis worker: cancel-mid-dispatch left the dashboard banner stuck
   on the cancelled page until a later page overwrote it. Add a
   `Cancelled` AnalysisEvent variant, publish on the cancel-release
   branch, wire the analysis and admin overview dashboards to clear
   on it. Per-page chip rolls back from `analyzing` to `queued`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-23 08:01:53 +02:00
parent 95b98eebf1
commit 651fb27522
9 changed files with 88 additions and 5 deletions

View File

@@ -894,4 +894,27 @@ async fn cron_shutdown_does_not_block_on_in_flight_metadata_pass(pool: PgPool) {
started.elapsed() < Duration::from_secs(5),
"shutdown took too long; cancel didn't propagate into the cron tick body"
);
// The advisory lock must be released so another replica's cron can
// pick up the slot. Without this assertion, a refactor that moved
// the `pg_advisory_unlock` under the cancel arm (instead of running
// unconditionally below the select!) would ship green — only to
// wedge multi-replica deployments where the second replica's cron
// would block on `pg_try_advisory_lock` forever. `pg_try_advisory_lock`
// returns true iff the lock is free, so we use it as the probe.
let acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
.bind(CRON_LOCK_KEY)
.fetch_one(&pool)
.await
.unwrap();
assert!(
acquired,
"advisory lock must be released after cancel-during-tick \
(otherwise multi-replica safety breaks)"
);
// Be a good citizen for whatever sqlx test runs next.
let _ = sqlx::query("SELECT pg_advisory_unlock($1)")
.bind(CRON_LOCK_KEY)
.execute(&pool)
.await;
}