Skip to content

Tracking: commit, checkpoint and WAL recovery fixes found by crash and stress testing #1376

Open

Description

@zahariash

This work was produced with the help of language models.

This collects the open fixes for commit, checkpoint and WAL recovery bugs, so they can be reviewed as a set. Each is its own draft PR against main with its own tests; they stay drafts, since one may change without notice to stay compatible with the others. This is a broad experiment aimed at improving durability under pressure. Backward compatibility is out of scope until a working solution is found.

Process. Bugs come from a workload simulator and the stress loop below. Each gets an issue with a deterministic repro, checked on main and the last release, and a fix PR whose tests fail on main. Because one bug can hide another (a segfault after a refused BEGIN ends a workload before a lost commit shows), the open PRs are also merged onto one main commit and stressed against main alone, with the same seeds.

Issue What goes wrong Fix State
#1199 a failed automatic checkpoint fails the committed statement, so retries apply it twice #1361 draft, review answered
#1316 after a refused BEGIN, the connection's next statement segfaults #1375 draft
#1357 opening with the other enableChecksums setting deletes the WAL #1358, ladybug-python#75 draft
#1304 after a checkpoint pinned by a reader, a crash or failed checkpoint loses later commits or blocks reopening #1378, #1379 (named graphs) draft
#1377 a COMMIT that fails on a full buffer pool is applied after a crash; a retried primary-key insert then blocks reopening #1380 draft
#1396 with a partitioned table, a checkpoint that fails (a full disk, a full buffer pool) segfaults while rolling back #1399 draft
#1397 after a WAL write fails on a full disk, the next commit hangs and closing the database aborts #1400 draft
#1406 after a failed checkpoint, the retry skips the tables it had finished, so their new primary keys, and when every table was done their new rows and edges, are lost after a reopen #1408 draft

Merged from the same testing: #1195, #1333, #1338, #1355.

Related, outside this set: #1389 fixes #1386, a segfault after a write or checkpoint is refused at max_db_size. #1361 turns that refused automatic checkpoint into a warning, so with #1361 the crash becomes the normal path; #1389 should land with or before it.

Merge order. Each PR is a fix of its own, but some depend on others or change the same lines:

  • #1389 with or before #1361: #1361 turns a checkpoint refused at max_db_size into a warning, so without #1389 the next statement segfaults (#1386).
  • #1378 before #1379, which is stacked on it and needs a rebase once #1378 is in.
  • #1380 after #1378: both change WALReplayer::dryReplay and the replay loops.
  • ladybug-python#75 after #1358 is in the engine the driver builds against: with the driver's new lenient default and without #1358, opening with the other checksum setting deletes the WAL.
  • #1375, #1358, #1399, #1400 and #1408 at any point.
  • #1358, #1378, #1379 and #1380 all change the WAL replay code and conflict in places, and #1400 conflicts with #1380 in WAL::logCommittedWAL; whichever lands second resolves it, as the merged branch of the runs below does.

A safe sequence: #1375, #1358, #1389, #1361, #1399, #1378, #1379, #1380, #1400, #1408, then ladybug-python#75.

Compatibility. Every fix except #1377's keeps the WAL and data-file formats. #1378 changes only the content of the pinned-checkpoint marker file, which older builds check just for existence. #1377's fix adds an ABORT WAL record that older builds cannot read; it lasts only until the next checkpoint retires the WAL.

Stress loop. A C program on the C API: writer threads add relationships tagged with unique ids, with a checkpoint after every commit, and rounds add a full 64 MB buffer pool, COPY, several writers, a write transaction held past the 5 s checkpoint wait, or a file-size limit that fails checkpoint writes. Each round ends with SIGKILL or a clean close; a fresh process then checks that every committed write is there exactly once.

Run 1, main 7bcc47d:

main main + #1361 + #1375
Rounds that segfaulted (#1316) 99 of 450 0 of 450
Writes applied twice by a retrying client (#1199) 277 0
COPY reported durable, then lost 140 0
Rounds that lost committed writes (#1304) 77 of 750 80 of 750

Run 2, #1378 on main 7bcc47d + #1358 + #1361 + #1375, 300 rounds with the same seeds: 2,884 committed writes lost in 42 rounds without it, none with it.

Run 3, #1380 on main 7bcc47d + #1358 + #1361 + #1375 + #1378, 300 rounds with the same seeds: rounds where a write reported as failed was there after reopening went from 4 to none, and no committed write was lost.

Run 4, all six PRs at their current heads merged on main 7bcc47d: 300 stress rounds without a violation, and the workload simulator (sessions, heavy, default and races, 40 workloads of 60 steps each) passed with the rules for these bugs enabled.

Run 5, the workload simulator with a rule for each of these bugs: a kill during the checkpoint after a pinned one, a file-size limit (a full disk), a max_db_size cap, a COMMIT on a full buffer pool followed by a crash, a reopen with the other checksum setting, and a named graph. Each rule fails on a build without its fix: #1316, #1357, #1199, #1396 and #1397 on main, #1304 without #1378, the named-graph case without #1379, #1386 without #1389. The exception is the full-pool COMMIT, which never failed in the simulator, so #1380 rests on its own tests and Run 3. With all eight PRs and #1389 at their current heads merged on main 4ca2237, the five simulator suites (sessions, heavy, default, races, recovery; 40 workloads of 60 steps) passed with every rule enabled, apart from #1406, an older bug (also in 0.21.2) that the recovery suite found. With its fix, #1408, merged too, the recovery suite passed with that rule enabled as well.

#1304: #1378 and #1379.

#1377: approach. When applying a transaction fails after its COMMIT record is durable (a full buffer pool, or a named graph dropped by a concurrent DROP GRAPH):

  • the fix appends and syncs an ABORT record naming that transaction's block in the WAL;
  • recovery skips aborted blocks, so a failed COMMIT is never applied, and the database stays writable;
  • only if the ABORT record itself cannot be written does the WAL refuse writes until restart.

#1377: rejected alternatives.

  • Refuse writes until restart and report the transaction as committed, so the next reopen applies it once. Simpler and needs no new WAL record, but:
    • a transient full buffer pool would stop every writer until a restart;
    • the restart would apply a transaction the client saw fail, and readers would see the rolled-back state until then;
    • it breaks the existing DROP GRAPH race test, whose failed commit recovery already skips.
  • Refuse writes only when recovery would apply the failed transaction: not safe, since one transaction can write to both a dropped graph and main.
  • Reserve the memory before writing the COMMIT record: not possible; the amount is only known while applying.
  • Let the buffer pool overcommit while a commit is applied: removes the out-of-memory cause, but changes pool policy and doesn't cover the DROP GRAPH race. Left as a possible follow-up.

Candidates, not fixed yet:

To do once these PRs settle:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions