Skip to content

Commit 4a1260c

Browse files
committed
test: serialise the tests that write shared, globally-scoped state
Two flakes, both concurrency artifacts wearing the costume of a product bug against a suite that shares one live Valkey: * retention. purge_mcp_calls_before is GLOBAL BY TIMESTAMP, not scoped to a principal, so the per-principal uid() namespace that isolates everything else isolates nothing here. The colon-bearing-principal test failed roughly two runs in three -- at "must actually be purged", or one line earlier at the read-back -- because the sibling retention test's purge_mcp_calls_before(1_000_001_000) had already deleted its row. That red reads exactly like a broken retention index while retention is fine. * the audit zset. busbar:audit is one global sorted set keyed by seq. audit_append_and_list_are_ordered_oldest_first takes max(seq)+1_000 and then requires that record still to be in list_audit_tail(2); the two siblings that write fixed 910/930-million seqs can land in between and push it out, failing an assertion about ORDERING. Pre-existing on dev, not introduced by the call log -- it just surfaces more now that more tests share the server. A lock rather than disjoint ts bands or seq ranges: any band picked would sit inside some future purge's cutoff, and the failure would come back looking like a product bug again. Both guards ignore mutex poisoning so one real failure does not bury itself under spurious ones. Verified: 6 consecutive full unit runs green, from ~2-in-3 red before.
1 parent 437a536 commit 4a1260c

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

store-valkey/src/tests.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,8 +1045,26 @@ fn denylist_add_and_list_round_trips() {
10451045

10461046
// ── Audit log (unchanged shape) ──────────────────────────────────────────────────────────────
10471047

1048+
/// Serialises every test that writes the SHARED, fleet-wide audit zset.
1049+
///
1050+
/// `busbar:audit` is one global sorted set keyed by seq, so unlike every other fixture in this file
1051+
/// it cannot be isolated by a `uid()` namespace. `audit_append_and_list_are_ordered_oldest_first`
1052+
/// takes `max(seq) + 1_000` and then requires that record to still be in `list_audit_tail(2)`; the
1053+
/// two sibling tests below write FIXED seqs in the 910/930-million range, so whichever of them lands
1054+
/// between that read and the tail read pushes the record out and fails an assertion about ORDERING
1055+
/// with something that is really a concurrency artifact. Pre-existing on dev — surfaced here because
1056+
/// this suite now runs more tests against one shared server, not because retention changed anything.
1057+
static AUDIT_SEQ_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1058+
1059+
/// Take `AUDIT_SEQ_LOCK`, ignoring poisoning, so one failing audit test does not convert the others
1060+
/// into spurious failures that bury the original.
1061+
fn audit_seq_guard() -> std::sync::MutexGuard<'static, ()> {
1062+
AUDIT_SEQ_LOCK.lock().unwrap_or_else(|e| e.into_inner())
1063+
}
1064+
10481065
#[test]
10491066
fn audit_append_and_list_are_ordered_oldest_first() {
1067+
let _serialised = audit_seq_guard();
10501068
let Some(store) = live_store() else { return };
10511069
// The audit zset is SHARED and persistent, and this test used to be the only writer of low
10521070
// seqs, so it could assume it owned the whole thing. It never really did: it only looked that
@@ -1273,6 +1291,7 @@ mod conformance {
12731291

12741292
#[test]
12751293
fn append_audit_duplicate_seq_is_ok_when_identical_and_an_error_when_different() {
1294+
let _serialised = super::audit_seq_guard();
12761295
let seq = 910_000_000u64 + (std::process::id() as u64 % 1_000_000);
12771296
let Some((store, _ns)) = setup("aud", seq) else {
12781297
return;
@@ -1350,6 +1369,7 @@ fn one_corrupt_credential_row_does_not_break_the_whole_hydration_delta() {
13501369
/// then a revocation on the original store which MUST land.
13511370
#[test]
13521371
fn a_refused_transaction_does_not_swallow_the_next_write() {
1372+
let _serialised = audit_seq_guard();
13531373
let Some(store) = live_store() else { return };
13541374
let Some(other) = live_store() else { return };
13551375
let seq = 930_000_000u64 + (std::process::id() as u64 % 1_000_000);
@@ -1509,11 +1529,34 @@ fn mcp_call_principals_are_enumerable_after_a_reconnect() {
15091529
);
15101530
}
15111531

1532+
/// Serialises the tests that call `purge_mcp_calls_before`.
1533+
///
1534+
/// Retention is GLOBAL BY TIMESTAMP, not scoped to a principal, so a per-principal `uid()` namespace
1535+
/// — which isolates every other test in this file — isolates nothing here: one test's purge deletes
1536+
/// another's rows out from under it if their ts bands overlap, and both retention tests deliberately
1537+
/// write in the same 1_000_000_1xx band because that is what their cutoffs are about. Observed, not
1538+
/// theorised: `retention_still_finds_a_principal_whose_id_contains_the_separator_characters` failed
1539+
/// roughly two runs in three, at "must actually be purged" (its row already deleted by the sibling's
1540+
/// `purge_mcp_calls_before(1_000_001_000)`) or one line earlier at the read-back — a red that looks
1541+
/// exactly like a broken retention index while retention is in fact fine.
1542+
///
1543+
/// A lock rather than disjoint ts bands: any band this test picked would still be inside SOME other
1544+
/// purge's cutoff the moment a third retention test is added, and the failure would come back
1545+
/// looking like a product bug again.
1546+
static MCP_RETENTION_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1547+
1548+
/// Take `MCP_RETENTION_LOCK`, ignoring poisoning: a panic in one retention test must not convert
1549+
/// every other one into a spurious failure that buries the original.
1550+
fn mcp_retention_guard() -> std::sync::MutexGuard<'static, ()> {
1551+
MCP_RETENTION_LOCK.lock().unwrap_or_else(|e| e.into_inner())
1552+
}
1553+
15121554
/// Retention must ACTUALLY DELETE and report a real count — a purge that returns a number it did
15131555
/// not perform is worse than one that reports nothing purged. It must also retire the principal
15141556
/// from the boot enumeration once its chain is empty.
15151557
#[test]
15161558
fn purge_mcp_calls_before_deletes_and_returns_a_real_count() {
1559+
let _serialised = mcp_retention_guard();
15171560
let Some(store) = live_store() else { return };
15181561
let p = uid("vk_mcp_purge");
15191562
// Retention is GLOBAL by ts, so this test cannot assert an exact global count against a shared
@@ -1605,6 +1648,7 @@ fn a_replayed_mcp_call_is_idempotent_but_a_forked_one_is_refused() {
16051648
/// silently mis-splits, and the failure would surface as a purge that quietly removed nothing.
16061649
#[test]
16071650
fn retention_still_finds_a_principal_whose_id_contains_the_separator_characters() {
1651+
let _serialised = mcp_retention_guard();
16081652
let Some(store) = live_store() else { return };
16091653
let p = format!("{}:with:colons", uid("vk_mcp_sep"));
16101654
store

0 commit comments

Comments
 (0)