Docs
View as markdown

WAL and checkpoints

The write-ahead log's segment and record format, what each commit appends, when a checkpoint runs, what replay does on open, and the torn-tail rule.

cli

Every commit to a relate store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary is appended as one record to the store's WAL Append-only log each commit goes to before it applies; under the default full durability mode it is synced first, so a crash loses no acknowledged write. Read more in the glossary (WAL) before it applies to the graph in memory. A checkpoint The step that writes a fresh snapshot of committed state and recycles old WAL segments: every 300 seconds by default, on clean exit, and on .checkpoint. Read more in the glossary writes the committed graph to the store's snapshot The committed graph state a checkpoint writes to disk, loaded on open before the WAL replays; not the snapshot durability mode or --read-only freshness. Read more in the glossary files and deletes the WAL segments the snapshot now covers. Opening a store loads the snapshot, then replays the WAL records written after it. The WAL's format is the same under every layout and codec. Storage and durability explains why the write path has this shape.

Synopsis

relate --database <path> [--wal-dir <dir>] [--checkpoint-interval-secs <n>]
       [--durability-mode full|everysecond|snapshot] [--read-only[=live]] [<statement>]
relate query --database <path> < <script>
relate info --database <path> [--json]
SettingDefaultMeaning
--checkpoint-interval-secs <n>300Seconds between timed checkpoints, on relate and relate query. 0 turns off the timer only; the other triggers still run
RELATE_CHECKPOINT_INTERVAL_SECS300The same period for an open that sets no interval, relate serve included; the flag wins over the variable. A value that is not a whole number is not an error: the period stays 300
--wal-dir <dir><database>/wal; <database>/.cypherlite/wal for the markdown codecWhere a directory store keeps its WAL. Refused for a .rl file; relate serve has no such flag
--durability-mode, RELATE_DURABILITY_MODEfullWhen a commit's record is synced to disk relative to its acknowledgment

Two dot commands of the interactive shell, also accepted as lines of a script piped to relate query, act on the WAL and checkpoints:

Dot commandEffect
.checkpointRuns a checkpoint now and prints nothing on success. Refused on a --read-only open
.infoPrints the fields of relate info from inside the session; its node and relationship counts are the live graph's, not the snapshot's

Files on disk

LayoutWAL locationSegment filesLock files
Directory store, parquet or json codec<database>/wal/, or the --wal-dir folderNNNNNNNN.log.cypherlite.lock and .cypherlite.writer in the WAL folder
Directory store, markdown codec<database>/.cypherlite/wal/, or the --wal-dir folderNNNNNNNN.log.cypherlite.lock and .cypherlite.writer in the WAL folder
.rl fileInside the file, as wal/ in the same treewal/NNNNNNNN.log inside the fileNone beside the file; a writer holds the .rl file itself
In memoryProcess memory; nothing on diskNot applicableNot applicable

The store's root also holds its own .cypherlite.lock and .cypherlite.writer. The manifest.json of the last checkpoint records the checkpoint's log sequence number (LSN) as checkpoint_lsn, with its commit_ts; Storage formats lists the snapshot files beside them.

Segment format

The WAL is a series of segment files. Exactly one, the segment with the highest id, is open for appends.

PartSizeContent
NameNot applicableThe segment id as 8 decimal digits, then .log. A new WAL starts at 00000001.log; each new segment takes the next id. Other file names in the folder are ignored
Magic8 bytesMDBWAL01; MDBWAL02 for a segment created while the store holds an ontology. Both are readable by this release
RecordsThe rest of the fileZero or more records, back to back, up to the end of the file

A segment that holds no records is 8 bytes long: its magic alone.

Record format

FieldSizeContent
Checksum4 bytes, little-endianCRC-32 of the payload
Length4 bytes, little-endianThe payload's length in bytes: at least 1, at most 67,108,864 (64 MiB). A limit: a larger commit is refused before anything is written
PayloadThe length in bytesOne JSON object with the keys lsn, commit_ts and ops
Payload keyContent
lsnThe record's log sequence number. It increases with every record, and replay applies only records with an LSN higher than the snapshot's checkpoint_lsn
commit_tsThe commit timestamp. It increases strictly from one record to the next across every segment, and is never 0
opsThe commit's physical changes: a node or relationship created or deleted, a property set or removed, or a label added or removed. Never Cypher text, so replay never re-runs a statement. The encoding of each entry belongs to the engine; a change to it comes with a new segment magic

What a commit appends

Statement or transactionWAL effect
A statement outside a transaction that changes the graphOne record
An explicit transaction (.begin … .commit in the CLI)One record at commit, holding every statement's changes
An explicit transaction with no changes, or one rolled backNothing
A statement that changes nothing: a read, or a write whose pattern matches nothingNothing
CALL { … } IN TRANSACTIONSOne record per batch
A commit whose record would exceed 64 MiBNothing; the statement fails with This statement tried to commit a single WAL record of at least <size>. …
A commit that sets an ontology while the open segment is MDBWAL01A new MDBWAL02 segment first, then one record in it
Any write on a --read-only openNothing; the write is refused

The record is appended to the open segment. When that segment then holds 64 MiB or 10,000 commits, it closes and a checkpoint starts. Under the default full durability mode When a commit counts as safe on disk: full (the default) syncs the WAL first, everysecond about once a second, and snapshot only at checkpoints. Read more in the glossary the record is synced to disk before the commit is acknowledged, and commits waiting together share one sync; everysecond syncs about once a second and snapshot never syncs on commit. Choose a durability mode states each mode's loss window.

What a checkpoint does

StepEffect
RotateThe open segment closes, if it holds records; new commits go to a segment with the next id
PinThe checkpoint takes the LSN of the newest closed segment's last record
Write the snapshotThe committed state up to that LSN is written through the store's codec (codecs), together with the schema text copy under .cypherlite/schema/
PublishFiles are written under a temporary name and renamed into place, manifest.json last; the manifest's checkpoint_lsn is the pinned LSN. A crash before this step leaves the previous snapshot in force
RecycleClosed segments whose last record's LSN is no higher than the checkpoint LSN are deleted

An in-memory store has no snapshot files: its automatic checkpoints only recycle the WAL, and .checkpoint writes a snapshot that lives in the process.

When a checkpoint runs

TriggerDefaultChanged with
The open segment reaches a size64 MiB (67,108,864 bytes)Fixed; a limit with no flag or variable
The open segment reaches a commit count10,000 commitsFixed; a limit with no flag or variable
The timer fires while the WAL holds commits the snapshot does notEvery 300 seconds--checkpoint-interval-secs, RELATE_CHECKPOINT_INTERVAL_SECS; 0 turns off the timer only. An in-memory store has no timer
A writable store closes cleanlyAlwaysNot configurable. Every clean exit of a writable CLI run checkpoints; a failure exits 1 with Error: final checkpoint failed: <reason>
.checkpointOn requestNot applicable
A writable open of a store at an older readable versionOnce, at the current format, after replayNot configurable (Upgrade a store)
relate cloneOnce, into the destinationNot applicable; the destination starts at the source's loaded LSN with an empty WAL

A --read-only open never checkpoints, in either freshness. A --read-only=live reader that finds a segment it still needed recycled by the writer's checkpoint reloads the current snapshot and the WAL that remains.

What replay does on open

StepWritable open--read-only open
Check the version markersBefore anything is written; a store from a newer release fails to openThe same
Load the snapshotThe state at its checkpoint_lsnThe same
WAL folder missingA new, empty WAL is created; the store opens at the snapshot's state, without the commits the missing segments heldFails: read-only open requires an existing WAL at <path>
Scan the segmentsEvery segment in id order. Every record is checked, including those up to the checkpoint LSN; records with a higher LSN are applied in LSN orderThe same
A torn tailDropped, then removed from its file once every segment has scanned; later appends go to a new segmentDropped; the file is left as it is
A newest segment shorter than its 8-byte magicRewritten as an empty MDBWAL01 segment, when lower-numbered segments exist and are intact and its bytes start that magicFails: truncated WAL magic: <segment>
Any other damaged recordThe open fails; no partial graph is served and no segment is recycledThe same
The graph size limit (--logical-graph-size-limit)Not applied to replay; an open always succeeds over itThe same
An older readable versionMigrated: a checkpoint at the current format, then a new segmentNot migrated
At exitA checkpoint on clean closeNo checkpoint

The torn-tail rule

A torn tail is the damage a crash leaves at the very end of a segment: a record whose writing stopped part of the way through. Its commit was never acknowledged under full durability. The rule decides which damage replay drops and which stops the open:

DamageWhereResult
An incomplete record at the end of the file: 1 to 7 header bytes, or a header whose payload runs past the endAny segmentTorn tail: dropped
A complete last record whose checksum failsThe open segmentTorn tail: dropped
A complete last record whose checksum failsA closed segmentThe open fails: WAL checksum mismatch
A record whose checksum fails, with more bytes after itAny segmentThe open fails: WAL checksum mismatch
A record whose checksum matches but whose payload does not decodeAny segment, any positionThe open fails: undecodable WAL record … may have been written by a newer Relate

Dropping a torn tail prints nothing. A writable open writes the segment's intact records to a new file and renames it over the old one, so a failure part of the way through leaves the original in place. Recover from a crash rehearses a crash and its recovery.

Examples

These commands write three commits to a new directory store in one script run, then print the store's metadata, the WAL folder and its one segment:

cat > team.cypher <<'EOF'
CREATE (:Person {name: 'Ada', role: 'engineer'});
CREATE (:Person {name: 'Grace', role: 'engineer'});
CREATE (:Person {name: 'Noor', role: 'maintainer'});
EOF
relate query --database ./team < team.cypher
relate info --database ./team
ls -A team/wal
cat team/wal/00000002.log; echo
{"planCache":"miss","timings":{"elapsed_ms":1.341,"execute_ms":1.173125,"lex_parse_ms":0.004666,"lower_ms":0.003875,"parse_ms":0.16037500000000002,"plan_ms":0.001708,"result_generation_ms":0.000209,"rewrite_ms":0.00025}}
{"planCache":"miss","timings":{"elapsed_ms":0.876416,"execute_ms":0.844,"lex_parse_ms":0.002209,"lower_ms":0.002,"parse_ms":0.030042000000000003,"plan_ms":0.001333,"result_generation_ms":0.000083,"rewrite_ms":0.000167}}
{"planCache":"miss","timings":{"elapsed_ms":0.8362499999999999,"execute_ms":0.795542,"lex_parse_ms":0.002375,"lower_ms":0.003791,"parse_ms":0.038416,"plan_ms":0.001458,"result_generation_ms":0.000042,"rewrite_ms":0.000209}}
layout: native directory
codec: parquet
format version: 3
checkpoint LSN: 3
commit timestamp: 3
total bytes: 2010
wal segments past checkpoint: 1
nodes (at checkpoint LSN 3): 3
relationships (at checkpoint LSN 3): 0
indexes: 0
constraints: 0
.cypherlite.lock
.cypherlite.writer
00000002.log
MDBWAL01

The run closed the store cleanly, so its checkpoint covers all three commits: checkpoint LSN: 3. The checkpoint recycled 00000001.log, and the open segment 00000002.log holds its magic and no records. wal segments past checkpoint counts the segment files that may hold commits newer than the snapshot, so an empty open segment counts as 1.

This script commits one relationship, prints the WAL and checkpoint fields, forces a checkpoint with .checkpoint, and prints them again:

cat > checkpoint.cypher <<'EOF'
MATCH (a:Person {name: 'Ada'}), (g:Person {name: 'Grace'}) CREATE (a)-[:KNOWS]->(g);
.info
.checkpoint
.info
EOF
relate query --database ./team < checkpoint.cypher | grep -E '^(checkpoint LSN|total bytes|wal segments)'
checkpoint LSN:               3
total bytes:                  2128
wal segments past checkpoint: 1
checkpoint LSN:               4
total bytes:                  3338
wal segments past checkpoint: 1

Before .checkpoint, the commit is in the WAL only: the checkpoint LSN is still 3, and the store is larger by the new record. After it, the checkpoint LSN is 4, the snapshot holds a relationships/ data file, and the WAL is back to one empty segment.

A single statement makes 10,001 commits with CALL { … } IN TRANSACTIONS OF 1 ROWS. Commit number 10,000 fills the open segment's commit count and starts a checkpoint:

relate query --database ./batch <<'EOF' | grep -E '^(checkpoint LSN|nodes)'
UNWIND range(1, 10001) AS i CALL (i) { CREATE (:Item {i: i}) } IN TRANSACTIONS OF 1 ROWS;
.info
EOF
relate info --database ./batch | grep -E '^(checkpoint LSN|nodes)'
checkpoint LSN:               10000
nodes:                        10001
checkpoint LSN: 10001
nodes (at checkpoint LSN 10001): 10001

Inside the session the checkpoint LSN is 10,000 while the live graph holds 10,001 nodes; the last commit is in the WAL. The checkpoint at exit covers it.

Errors

The first seven errors stop the open with exit code 1. On stderr each is prefixed Failed to open Relate: , and in --mode envelope it is an error object with kind storage. <segment> is the segment's name, such as 00000002.log.

MessageExit codeWhen
storage error: WAL checksum mismatch1A complete record fails its checksum with bytes after it, or as the last record of a closed segment (storage errors)
storage error: undecodable WAL record at offset <n> in <segment>: may have been written by a newer Relate (<detail>)1A record's checksum matches but its payload does not decode; a newer release wrote it (Upgrade a store)
storage error: invalid WAL magic: <segment>1A segment's first 8 bytes are neither MDBWAL01 nor MDBWAL02
storage error: truncated WAL magic: <segment>1A segment is shorter than its 8-byte magic, and is not a newest segment a writable open can rewrite
storage error: invalid WAL payload length <n> in <segment> (limit 64 MiB)1A record's length field is 0 or larger than 67,108,864
storage error: non-increasing or unsupported WAL commit timestamp <ts> after <previous> in <segment>1A record's commit_ts is 0, or not larger than the previous record's
storage error: ontology op in non-MDBWAL02 WAL segment <segment> at offset <n>1A record that changes the ontology sits in an MDBWAL01 segment
Error: final checkpoint failed: <reason>1The checkpoint at a clean exit failed (details)
semantic error: WriteOnReadOnlyAccessDatabase: No write operations are allowed on this database. The database is in read-only mode on this Neo4j instance.1.checkpoint on a --read-only open (details)

The errors at open that concern the WAL folder itself, such as a --read-only open without one or --wal-dir on a .rl path, are in the storage formats errors. The errors reference lists the storage errors with their remedies.

See also