Skip to content

Fix short writes on a full PERSIST volume losing acknowledged items - #163

Merged
jonbaldie merged 1 commit into
mainfrom
fix/160-short-writes
Oct 10, 2026
Merged

jonbaldie merged 1 commit into
mainfrom
fix/160-short-writes

Conversation

@jonbaldie

Copy link
Copy Markdown
Owner

Closes #160

Root cause

FileStore ignored the byte count returned by Deno.FsFile.writeSync(). When the volume is full, the OS does a short write: it returns fewer bytes and no error, and only the next write fails (probe: first write 102400 of 204800, then File too large (os error 27)). So:

  • Runtime appends: the partial line was acknowledged with 200. The next event was then glued onto it, and replay dropped both.
  • Snapshots (replace): the truncated temp file was fsynced and renamed over a complete persist.dat.

Fix

  • FileStore.writeAll loops until every byte is written, so the OS error comes through.
  • A failed append is rolled back to the previous end of the log (truncate, then seek; on macOS the stale fd offset still trips the size limit). The log stays line-aligned.
  • replace throws before the rename, so the old persist.dat stays. On shutdown, main.ts logs Failed to flush data to persist.dat: … and exits 1 instead of printing Goodbye!.
  • Manager.enqueue/dequeue write to the log before changing memory. A request that can't be persisted returns 500 and leaves the queue unchanged.

Verification

  • New tests/short_write_test.ts runs the real server under ulimit -f (with SIGXFSZ ignored). That forces real short writes and works on CI without root. Both tests failed on main, matching the issue's two symptoms, and pass on this branch (3/3 runs).
  • Re-ran the issue's original scripts against a binary built with deno compile on a 2 MiB HFS+ RAM disk (real ENOSPC):
    • journey2_diskfull.sh: the 200 KB enqueue gets 500, and after SIGKILL and restart both before-full and after-space-freed are recovered.
    • journey2_snapshot_nearfull.sh: exit status 1, persist.dat stays at 200123 bytes, and both items are recovered.
  • Full deno test locally: 374 passed. The only failure is the Stryker runner test, because Stryker isn't installed locally; CI installs it.

🤖 Generated with Claude Code

…160)

Root cause: FileStore ignored the byte count returned by
Deno.FsFile.writeSync(). On a full volume the OS performs a short write
and returns fewer bytes without an error, so a partial log line was
reported as success. The next append was glued onto it and replay
dropped both; the shutdown snapshot was fsynced and renamed over a
complete persist.dat while truncated.

- Write every byte or throw (EFBIG/ENOSPC surfaces on the retry).
- Roll a failed append back to the previous end of the log (truncate
  and seek), so the log stays line-aligned.
- A snapshot that cannot be fully written throws before the rename,
  keeping the old persist.dat; shutdown logs the error and exits 1.
- Manager logs enqueue/dequeue before mutating memory, so a request
  that cannot be persisted fails (500) without changing queue state.

Regression tests use `ulimit -f` to force real short writes without
needing root to mount a tiny volume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jonbaldie
jonbaldie merged commit c67991d into main Oct 10, 2026
4 checks passed
@jonbaldie
jonbaldie deleted the fix/160-short-writes branch October 10, 2026 04:48
jonbaldie added a commit that referenced this pull request Oct 10, 2026
Enqueue and dequeue already write the log before changing memory (#163).
A failed write therefore returns 500 and leaves the queue unchanged.
Nothing covered a write that fails outright once persist.dat is already
at the size cap, so reversing that order would again drop an item on a
500 dequeue and keep a rejected enqueue.

The regression test fills the log to the ulimit cap, then checks that a
500 dequeue and a 500 enqueue leave length unchanged and that only the
accepted item is recovered after restart.
@jonbaldie jonbaldie mentioned this pull request Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Short writes on a full PERSIST volume are reported as success and corrupt persist.dat, losing acknowledged items

1 participant