Skip to content

Bug: lenient WAL replay drops intact commits after a damaged record #1497

Open

Description

@zahariash

This work was produced with the help of language models.

With throw_on_wal_replay_failure=False, the engine's default and Python's from its next release (ladybug-python#77), recovery truncates the WAL at the first record it cannot read, which suits a torn tail but loses every intact commit after damage in the middle.

On 0.22.1, after one byte of commit 2 of 3 is flipped (first script below), every lenient open shows only commit 1, [[1, 100]], while a strict open refuses with Checksum verification failed, the WAL file is corrupted. and keeps the WAL.

A read that fails once (an EIO injected on 0.22.0; 0.22.1 has the same replay code) drops the intact commits after it the same way, and so does a record of a type the build does not know, such as the ABORT record #1380 adds.

A read-only transaction open during a checkpoint keeps its CHECKPOINT record in the WAL. Damage before that record makes recovery drop it and apply earlier commits again: with commits adding k = 1, 2 and 3, the record between the last two, and one byte of commit 2 flipped, a lenient open on 0.22.1 returns k = 1, 1, 2 with no error (second script below).

Expected: lenient replay drops only a torn tail and refuses anything else, keeping the WAL.

Repro (Python, ladybug==0.22.1)
import os
import tempfile

import ladybug as lb

path = os.path.join(tempfile.mkdtemp(), "db")
with lb.Database(path) as db, lb.Connection(db) as conn:
    conn.execute("CALL force_checkpoint_on_close=false")
    conn.execute("CALL auto_checkpoint=false")
    conn.execute("CREATE NODE TABLE T(id INT64 PRIMARY KEY)")
    sizes = []
    for commit in (1, 2, 3):
        conn.execute(f"UNWIND range({commit * 1000}, {commit * 1000 + 99}) AS i CREATE (:T {{id: i}})")
        sizes.append(os.path.getsize(path + ".wal"))

offset = (sizes[0] + sizes[1]) // 2
with open(path + ".wal", "r+b") as wal:
    wal.seek(offset)
    byte = wal.read(1)[0]
    wal.seek(offset)
    wal.write(bytes([byte ^ 0xFF]))

query = "MATCH (t:T) RETURN t.id / 1000 AS c, count(*) ORDER BY c"
for label, strict in (("strict open", True), ("lenient open", False), ("reopen", False)):
    try:
        with lb.Database(path, throw_on_wal_replay_failure=strict) as db, lb.Connection(db) as conn:
            print(f"{label}: {conn.execute(query).get_all()}")
    except RuntimeError as e:
        print(f"{label}: {e}")

Output:

strict open: Storage exception: Checksum verification failed, the WAL file is corrupted.
lenient open: [[1, 100]]
reopen: [[1, 100]]
Repro with a CHECKPOINT record (Python, ladybug==0.22.1)
import os
import tempfile

import ladybug as lb

path = os.path.join(tempfile.mkdtemp(), "db")
create = "MATCH (a:T {{id: 1}}) CREATE (a)-[:R {{k: {}}}]->(a)"
with lb.Database(path) as db:
    conn = lb.Connection(db)
    conn.execute("CALL force_checkpoint_on_close=false")
    conn.execute("CALL auto_checkpoint=false")
    conn.execute("CREATE NODE TABLE T(id INT64 PRIMARY KEY)")
    conn.execute("CREATE REL TABLE R(FROM T TO T, k INT64)")
    conn.execute("CREATE (:T {id: 1})")
    conn.execute("CHECKPOINT")
    conn.execute(create.format(1))
    commit2_start = os.path.getsize(path + ".wal")
    conn.execute(create.format(2))
    reader = lb.Connection(db)
    reader.execute("BEGIN TRANSACTION READ ONLY")
    conn.execute("CHECKPOINT")
    conn.execute(create.format(3))
    reader.execute("COMMIT")
    reader.close()
    conn.close()

with open(path + ".wal", "r+b") as wal:
    wal.seek(commit2_start + 9)
    byte = wal.read(1)[0]
    wal.seek(commit2_start + 9)
    wal.write(bytes([byte ^ 0xFF]))

query = "MATCH (:T)-[r:R]->(:T) RETURN r.k ORDER BY r.k"
for label, strict in (("strict open", True), ("lenient open", False), ("reopen", False)):
    try:
        with lb.Database(path, throw_on_wal_replay_failure=strict) as db, lb.Connection(db) as conn:
            print(f"{label}: {conn.execute(query).get_all()}")
    except RuntimeError as e:
        print(f"{label}: {e}")

Output:

strict open: Storage exception: Checksum verification failed, the WAL file is corrupted.
lenient open: [[1], [1], [2]]
reopen: [[1], [1], [2]]

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