Lines
100 %
Functions
Branches
//! Overlapping runs must not overwrite each other's verdicts.
//!
//! Two releases' migrators can be in flight at once — a retried old Job beside a
//! new one. Each run fences its writes on `users.verdict_at`, so a run may only
//! rule on a tenant whose last verdict predates it.
//! The fence deliberately is NOT `quarantined_at`: clearing a quarantine nulls
//! that column, which would erase the fence and let an older failure land on a
//! tenant a newer run had just verified.
#![cfg(feature = "db")]
use sqlx::Row;
use tests_integration::fleet_support::{admin_connect, forget_tenant, register_tenant};
use uuid::Uuid;
async fn verdict_state(admin: &sqlx::PgPool, id: Uuid) -> (Option<String>, bool) {
let row =
sqlx::query("SELECT quarantine_reason, verdict_at IS NOT NULL FROM users WHERE id = $1")
.bind(id)
.fetch_one(admin)
.await
.expect("read verdict");
(row.get(0), row.get(1))
}
/// Applies the shipped SQL directly, so the test exercises the real statements
/// rather than a paraphrase of them.
async fn quarantine(admin: &sqlx::PgPool, id: Uuid, reason: &str, run: &str) -> u64 {
sqlx::query(include_str!("../../server/sql/update/users/quarantine.sql"))
.bind(reason)
.bind(sqlx::types::chrono::DateTime::parse_from_rfc3339(run).expect("timestamp"))
.execute(admin)
.expect("quarantine")
.rows_affected()
async fn clear(admin: &sqlx::PgPool, id: Uuid, run: &str) -> u64 {
sqlx::query(include_str!(
"../../server/sql/update/users/clear_quarantine.sql"
))
.expect("clear")
const OLD_RUN: &str = "2020-01-01T00:00:00Z";
const NEW_RUN: &str = "2099-01-01T00:00:00Z";
#[tokio::test]
async fn an_older_run_cannot_overwrite_a_newer_verdict() -> anyhow::Result<()> {
let admin = admin_connect().await;
let (id, database_name) = register_tenant(&admin, "fence", false).await;
// A newer run verifies the tenant and lifts any quarantine.
assert_eq!(clear(&admin, id, NEW_RUN).await, 1);
let (reason, has_verdict) = verdict_state(&admin, id).await;
assert_eq!(reason, None);
assert!(
has_verdict,
"a clear records a verdict, it does not erase one"
);
// An older run, still in flight, now fails on that same tenant. Fencing on
// `quarantined_at` would admit this write, because the clear set it to NULL.
assert_eq!(
quarantine(&admin, id, "stale failure", OLD_RUN).await,
0,
"an older run must not re-quarantine a tenant a newer run just verified"
assert_eq!(verdict_state(&admin, id).await.0, None);
forget_tenant(&admin, id, &database_name).await;
Ok(())
async fn an_older_run_cannot_clear_a_newer_quarantine() -> anyhow::Result<()> {
let (id, database_name) = register_tenant(&admin, "fence-inverse", false).await;
assert_eq!(quarantine(&admin, id, "newer failure", NEW_RUN).await, 1);
clear(&admin, id, OLD_RUN).await,
"an older success must not release a tenant a newer run has condemned"
verdict_state(&admin, id).await.0,
Some("newer failure".to_owned())
async fn a_newer_run_always_wins() -> anyhow::Result<()> {
let (id, database_name) = register_tenant(&admin, "fence-newer", false).await;
assert_eq!(quarantine(&admin, id, "old failure", OLD_RUN).await, 1);
clear(&admin, id, NEW_RUN).await,
1,
"the fence must not become a deadlock: a newer run still rules"
async fn a_late_publication_from_an_older_run_cannot_block_a_newer_one() -> anyhow::Result<()> {
// The realistic overlap, and the one the first version of this fence got
// wrong: BOTH runs start before EITHER publishes. The old run then writes
// last in wall-clock terms.
//
// Storing the publication instant while comparing against a run's start
// made the old run stamp a time later than the new run's start, so the new
// run's verdict was refused and the tenant kept the stale one. Both sides
// must be the run's own start.
let (id, database_name) = register_tenant(&admin, "fence-late", false).await;
const OLDER_START: &str = "2030-01-01T00:00:00Z";
const NEWER_START: &str = "2030-06-01T00:00:00Z";
// The older run publishes first, but it started earlier.
quarantine(&admin, id, "stale failure", OLDER_START).await,
1
// The newer run, which started after it, must still be able to rule.
clear(&admin, id, NEWER_START).await,
"a newer run must not be blocked by an older run that happened to write first"
// And the older run, publishing again even later, still cannot overwrite it.
quarantine(&admin, id, "stale again", OLDER_START).await,
"writing later in wall-clock time must not promote an older run"