Skip to content

Cache.load() leaves the cache partially initialized when WAL replay fails #341

Closed

Description

@TynK-M

What happened?

Cache.load() updates the cache's internal state before replaying the write-ahead log (WAL). If WriteAheadLog.replay() raises an OSError, the method exits with an exception, but some internal attributes have already been changed.

Specifically, self.db, self.filename, and self.wal are assigned before WAL replay completes. As a result, a failed load can leave the cache appearing initialized even though the WAL entries have not been fully recovered.

If the application catches the exception and continues using the same cache instance, subsequent API calls may operate on an incomplete database instead of retrying initialization.

This can lead to committed writes being temporarily invisible. If subsequent writes trigger compaction, the incomplete state may potentially be persisted, causing permanent data loss.

Steps to reproduce

  1. Create a database with a valid snapshot and a WAL containing a committed write that has not yet been included in the snapshot.
  2. Create a fresh Cache instance.
  3. Temporarily make WriteAheadLog.replay() raise an OSError, simulating a transient WAL read failure.
  4. Call cache.load(filename) and catch the exception.
  5. Restore normal WAL replay behavior.
  6. Inspect the cache state and attempt to load the same database again or access it through the public API.
import json
import tempfile
from pathlib import Path

from seriousdb.cache import Cache
from seriousdb.wal import WriteAheadLog

with tempfile.TemporaryDirectory() as tmp:
    filename = str(Path(tmp) / "test.db")

    # Create a valid database snapshot.
    Path(filename).write_text(
        json.dumps({"existing": "snapshot data"}),
        encoding="utf-8",
    )

    original_replay = WriteAheadLog.replay

    def fail_replay_once(wal):
        WriteAheadLog.replay = original_replay
        raise OSError("simulated transient WAL read failure")

    WriteAheadLog.replay = fail_replay_once

    cache = Cache()

    try:
        cache.load(filename)
    except OSError as exc:
        print(f"Load failed as expected: {exc}")
    finally:
        WriteAheadLog.replay = original_replay

    print("Cache state after failed load:")
    print(f"  db:       {cache.db}")
    print(f"  filename: {cache.filename}")
    print(f"  wal:      {cache.wal}")

    # Current implementation: these assertions demonstrate the bug.
    assert cache.db is not None, "Expected to reproduce partial initialization"
    assert cache.filename == filename
    assert cache.wal is not None

    print("\nBUG REPRODUCED: cache appears initialized despite load failure.")

After the exception, inspect cache.db, cache.filename, and cache.wal. The cache may have been partially initialized despite the failed load.

What did you expect?

If loading fails during WAL replay, the cache should not be left in a state that appears successfully initialized but contains only partially recovered data.

A subsequent load attempt should be able to recover all committed writes, and the cache should not silently serve incomplete data or persist an incomplete database state.

Environment

seriousdb version: 1ea220c
Operating system: WSL (Ubuntu distro)
Run method: uv

Additional context

If possible I want to be assigned to this issue.

Activity

  1. mattsva commented on Oct 12, 2026

    @mattsva
    Collaborator

    I could reprocess it with nix-shell -p python3 uv then the given code with uv and seriousdb 1ea220c.
    Thanks for reporting and I assign you @TynK-M .

  2. mattsva commented on Oct 12, 2026

    @mattsva
    Collaborator

    @TynK-M If you do have a PR, you can ask me for review if you want.

  3. mattsva commented on Oct 12, 2026

    @mattsva
    Collaborator

    #342 closes this.

    @TynK-M next time please say smth like "closes #PR" or "fixes #PR" in the PR description, to close the issue automatically.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions