8000 test: replay one heavy WAL entry through Minigraf::open (#865) by adityamukho · Pull Request #867 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/acceptance/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
['cartesian_k13_streams', 'count_holds_no_rows'],
),
'limit': (['limit_and_top_k_over_10m_facts'], ['limit_10m', 'top_k_10m']),
'large-transaction': (['one_million_fact_transact_holds_one_chunk', 'custom_chunk_bounds'], []),
'large-transaction': (
['one_million_fact_transact_holds_one_chunk', 'custom_chunk_bounds', 'one_million_fact_wal_entry_replays_on_open'],
[],
),
'size-per-fact': (['storage::persistent_facts::tests::size_per_fact_in_433_shape_1m'], []),
}

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Documentation

- **Heavy WAL entry replay test** (#865): a nightly acceptance test writes one WAL entry of 1,000,000 facts, reopens without a checkpoint and checks every replayed fact through entity, attribute and attribute-value lookups, then checkpoints, verifies and reopens. It passes; no production change.
- **Failure-path correctness map** (#843): [X08's correctness-target map](docs/benchmarks/843-failure-correctness/README.md) ties invalid bounds, windows and binds, callback errors, exhaustion, corrupt pages, failed-handle retry, v1–v6 refusal and heavy single-entry WAL replay to 138 named tests at a pinned v3 revision, with their error and data invariants, and limits the C08 repair and C30 rollback claims to their fixtures. Gaps found: heavy WAL replay through `Minigraf::open` (#865) and the recursion round bound (#861). X08 stays excluded. No production, test, public API or nightly-route change.

- **Semantic permutation map** (#842): [X07's correctness-target map](docs/benchmarks/842-semantic-permutations/README.md) ties numeric, bind, cross-kind join and forced-planner permutations to 128 named tests at a pinned v3 revision, states what the representative nightly controls do and do not establish, and records the hash-scan and keyword binding/clone confounders. X07 stays excluded. No production, test, public API or nightly-route change.
Expand Down
10 changes: 5 additions & 5 deletions docs/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
| | Default features | With `--features test-hooks` |
|---|---|---|
| **Active** | 2,041 | 2,076 |
| **Ignored** | 21 | 23 |
| **Total** | 2,062 | 2,099 |
| **Ignored** | 21 | 24 |
| **Total** | 2,062 | 2,100 |

Counts above are the current Cargo test inventory (active versus ignored),
including #777, #789, #781, #794, #795 and #837 (Linux). Verification results below identify which suites ran.
including #777, #789, #781, #794, #795, #837 and #865 (Linux). Verification results below identify which suites ran.

**#838 manual migration verification**: the [manual sweep](benchmarks/838-migration/README.md)
checks every fixture value and cursor snapshot before accepting timings, including
Expand Down Expand Up @@ -415,7 +415,7 @@ These are not `#[test]`s; run them with `cargo kani` or `.github/workflows/kani.
- Cursors and fact logs opened before and between chunks keep their snapshot through later checkpoints.
- The fault matrix injects every write and sync fault at every call of a large transaction, on a file with one and with two valid meta slots, with and without a WAL beforehand.
- Allocator tracking and release, cache eviction, and cursor peeking have unit tests.
- `tests/large_transaction_acceptance_test.rs` (ignored, `test-hooks` only, nightly in `.github/workflows/smoke.yml`) holds at most 65,536 materialised facts (the default chunk) for a 1M-fact `transact`, and prints peak RSS. `custom_chunk_bounds` checks public chunk sizes 0, 1, 2, 257, 131,072, 1,000,000 and `usize::MAX`, including million-fact custom chunks, exact retained-fact peaks and complete counts after reopen.
- `tests/large_transaction_acceptance_test.rs` (ignored, `test-hooks` only, nightly in `.github/workflows/smoke.yml`) holds at most 65,536 materialised facts (the default chunk) for a 1M-fact `transact`, and prints peak RSS. `custom_chunk_bounds` checks public chunk sizes 0, 1, 2, 257, 131,072, 1,000,000 and `usize::MAX`, including million-fact custom chunks, exact retained-fact peaks and complete counts after reopen. `one_million_fact_wal_entry_replays_on_open` (#865) writes one WAL entry of exactly 1,000,000 facts (`transaction_chunk_facts(1_000_000)`) and a small second entry, closes without a checkpoint (`wal_checkpoint_threshold(usize::MAX)`), and checks after `Minigraf::open` replay, after `checkpoint` + `verify()`, and after a second reopen: entity, attribute-scan and attribute-value lookups, `current_tx_count() == 2` and the `:as-of 1` boundary. A frozen v2.x fixture with a larger entry was not added: its WAL alone is about 50 MB.
- **Statement bases and large write transactions** (`src/storage/persistent_facts/large_tx.rs`, 9 tests; `src/fault_matrix.rs`, 4 tests; `src/storage/page.rs` and `src/storage/btree.rs`, 2 tests; `tests/memory_limit_test.rs`, 1 test changed)
- A `WriteTransaction` whose statements repeat, change, retract and re-assert earlier statements' facts within and across chunks, at chunk sizes 1 to 4, gives every statement the small path's outcome and stores the same records. This includes two statements that fail `API-011`, one only through a repeat of an earlier statement's fact (the claims case). Checked over one transaction and over two with committed and pending facts between them; passes `verify` and survives reopen.
- Queries after each statement, inside the transaction at chunk sizes none and 1 to 4, return what a commit of the statements so far holds, including a staged retraction of a pending assertion and the reverse.
Expand Down Expand Up @@ -691,7 +691,7 @@ The twenty-one ignored tests (twenty-three with `test-hooks`) are intentionally
- 13 patterns sharing no variable over the fuzz harness's 3 facts stream 3^13 rows with peak RSS under 64 MiB, and with a 64-byte limit stop with `QRY-012` naming `HashJoin`;
- `count` over every one of 1M facts counts them under a 4 KiB `query_memory_limit` (`count_holds_no_rows`, the #473 constant-memory `count` item).
- One `:limit` and top-k acceptance test (`tests/limit_acceptance_test.rs`, #306, the #473 `:limit` item), run by the nightly smoke workflow over one 10M-fact fixture written with `LogWriter`: `:limit 10` returns 10 rows in under 250 ms under a 64 KiB `query_memory_limit`, adding under 32 MiB of peak RSS; `:order-by [?v :desc] :limit 10` returns the ten largest values under a 1 MiB limit, adding under 32 MiB.
- Two large-transaction acceptance tests (`tests/large_transaction_acceptance_test.rs`, #550, #837; `test-hooks` only), run by the nightly smoke workflow (see [Large transactions](#large-transactions-550)).
- Three large-transaction acceptance tests (`tests/large_transaction_acceptance_test.rs`, #550, #837, #865; `test-hooks` only), run by the nightly smoke workflow (see [Large transactions](#large-transactions-550)).
- One measurement test (`meter_peak_against_rss`, `src/query/engine/tests.rs`, #549): prints the meter's peak against the peak RSS for the same two shapes, opening the engine directly.
- One committed-group measurement test (`committed_group_memory_probe`, `src/query/engine/aggregate_order_tests.rs`, #641): opens prebuilt fixed-size and growing-group fixtures, drains count/count-distinct with and without a limit, and prints meter peaks, active-group high-water and input counts.
- Two generalized group measurement tests (`generalized_committed_memory_probe` and `certified_growing_distinct_memory_probe`, `src/query/engine/aggregate_order_tests.rs`, #781): drain prebuilt fixed-group and growing-distinct fixtures, reporting Aggregate and IdSpace peaks separately, input counts and exact results. See the [100K–10M evidence](benchmarks/781-ordered-aggregate.md).
Expand Down
5 changes: 5 additions & 0 deletions docs/benchmarks/843-failure-correctness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ that a v7 → v8 migration replays. #865 proposes a correctness test only, likel
among the ignored nightly acceptance tests. No production change is expected
unless that test fails.

Follow-up: `one_million_fact_wal_entry_replays_on_open` in
`tests/large_transaction_acceptance_test.rs` now covers this path (#865,
nightly); it passes with no production change. The map above stays pinned to
`a95e51ffe`.

**The 1,000-round recursion bound has no test at the pin.** `INT-020`
"Max iterations (1000) exceeded" (`engine/recursive.rs`) is not triggered by
any test. A check on the alternate host at the pin confirmed it is reachable:
Expand Down
148 changes: 147 additions & 1 deletion tests/large_transaction_acceptance_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Acceptance test for large transactions (#550 spec §8.1, #473 spec §8): a
//! 1M-fact `transact` on a file database holds at most `TX_CHUNK_FACTS`
//! materialised facts at any time, and commits as a checkpoint.
//! materialised facts at any time, and commits as a checkpoint. A statement
//! within a raised chunk writes one heavy WAL entry instead, which a reopen
//! replays (#865).
//!
//! Ignored by default; `.github/workflows/smoke.yml` runs it nightly:
//!
Expand Down Expand Up @@ -160,3 +162,147 @@ fn custom_chunk_bounds() {
assert_eq!(db.current_tx_count(), 1);
}
}

fn count_value(db: &Minigraf, query: &str) -> i64 {
let QueryResult::QueryResults { results, .. } = db.execute(query).unwrap() else {
panic!("expect 8000 ed count result");
};
assert_eq!(results.len(), 1, "one count row");
match results[0][0] {
minigraf::Value::Integer(n) => n,
_ => panic!("count is not an integer"),
}
}

/// Check both WAL entries of `one_million_fact_wal_entry_replays_on_open`
/// through the EAVT (entity bound), AEVT (attribute scan) and AVET (attribute
/// and value bound) paths, and the transaction boundary between them.
fn check_heavy_entry(db: &Minigraf, ctx: &str) {
assert_eq!(db.current_tx_count(), 2, "{ctx}: two transactions");
assert_eq!(
count_value(db, "(query [:find (count ?v) :where [:e10 :n ?v]])"),
1,
"{ctx}: EAVT"
);
assert_eq!(
count(db, "(query [:find ?e :where [?e :n 10]])"),
1,
"{ctx}: EAVT value"
);
assert_eq!(
count_value(db, "(query [:find (count ?e) :where [?e :n ?v]])"),
(FACTS / 2) as i64,
"{ctx}: AEVT :n"
);
assert_eq!(
count_value(db, "(query [:find (count ?v) :where [?e :m ?v]])"),
(FACTS / 2) as i64,
"{ctx}: AEVT :m"
);
assert_eq!(
count(db, "(query [:find ?v :where [:hub1 :m ?v]])"),
FACTS / 64,
"{ctx}: repeated entity"
);
assert_eq!(
count(db, "(query [:find ?e :where [?e :n 999998]])"),
1,
"{ctx}: AVET last"
);
assert_eq!(
count(db, "(query [:find ?e :where [?e :n 0]])"),
1,
"{ctx}: AVET first"
);
assert_eq!(
count(db, "(query [:find ?e :where [?e :m 999999]])"),
1,
"{ctx}: AVET :m"
);
assert_eq!(
count(db, r#"(query [:find ?e :where [?e :x "after"]])"#),
1,
"{ctx}: second entry"
);
assert_eq!(
count(db, "(query [:find ?v :as-of 1 :where [:small :x ?v]])"),
0,
"{ctx}: second entry is after tx 1"
);
assert_eq!(
count_value(db, "(query [:find (count ?e) :as-of 1 :where [?e :n ?v]])"),
(FACTS / 2) as i64,
"{ctx}: first entry is whole at tx 1"
);
}

/// #865: a database whose WAL holds one heavy entry replays it through
/// `Minigraf::open`. One `transact` of exactly 1,000,000 facts with
/// `transaction_chunk_facts(1_000_000)` stays a small statement and writes
/// one WAL entry; a second small statement writes another. The handle is
/// closed without a checkpoint: `wal_checkpoint_threshold(usize::MAX)` makes
/// the drop discard the storage instead of saving it, so the file holds no
/// facts and the reopen must replay both entries. The replayed facts are
/// checked, then checkpointed, verified and reopened again.
#[test]
#[ignore]
fn one_million_fact_wal_entry_replays_on_open() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("heavy-wal.graph");
let mut wal = path.as_os_str().to_owned();
wal.push(".wal");
let wal = std::path::PathBuf::from(wal);
let open_no_checkpoint = || {
minigraf::OpenOptions::new()
.transaction_chunk_facts(1_000_000)
.wal_checkpoint_threshold(usize::MAX)
.path(&path)
.open()
.unwrap()
};

let db = open_no_checkpoint();
let mut input = String::with_capacity(FACTS * 26);
input.push_str("(transact [");
for i in 0..FACTS {
if i % 2 == 0 {
input.push_str(&format!("[:e{i} :n {i}]"));
} else {
input.push_str(&format!("[:hub{} :m {i}]", i % 64));
}
}
input.push_str("])");
let start = std::time::Instant::now();
db.execute(&input).unwrap();
drop(input);
let written = start.elapsed();
assert!(wal.exists(), "the heavy statement stayed in the WAL");
db.execute(r#"(transact [[:small :x "after"]])"#).unwrap();
assert_eq!(db.current_tx_count(), 2);
assert_eq!(db.verify().unwrap().facts, 0, "nothing checkpointed");
drop(db);
let wal_bytes = std::fs::metadata(&wal).unwrap().len();

let start = std::time::Instant::now();
let db = open_no_checkpoint();
let replayed = start.elapsed();
assert_eq!(db.verify().unwrap().facts, 0, "replayed, not checkpointed");
check_heavy_entry(&db, "after replay");

db.checkpoint().unwrap();
assert!(!wal.exists(), "checkpoint removed the WAL");
let report = db.verify().unwrap();
assert!(report.is_ok(), "file intact after checkpoint");
assert_eq!(report.facts, FACTS as u64 + 1, "every fact checkpointed");
check_heavy_entry(&db, "after checkpoint");
drop(db);

let db = Minigraf::open(&path).unwrap();
assert!(!wal.exists(), "no WAL after the checkpointed reopen");
check_heavy_entry(&db, "after reopen");
eprintln!(
"one_million_fact_wal_entry_replays_on_open: {FACTS} facts written in {written:.2?}; \
WAL {} MiB; replayed on open in {replayed:.2?}",
wal_bytes / (1024 * 1024)
);
}
Loading
0