Skip to content

A re-import finds where the store ends in the file by comparing its ends - #201

Merged
torstei merged 1 commit into
mainfrom
bugfix/import-resume-by-tail
Oct 3, 2026
Merged

torstei merged 1 commit into
mainfrom
bugfix/import-resume-by-tail

Conversation

@torstei

@torstei torstei commented Oct 3, 2026

Copy link
Copy Markdown
Owner

The first layer of docs/plans/import-resume.md: Step 0, the aligned candidate with a byte verification. No hint file and no search yet.

Problem

Resuming a grown file checked that the file's first stamp equalled the store's first chunk's, seeked to size(), and decompressed every imported chunk to compare it with the file (--quick: the first, middle and last). Three things were wrong with that:

  • The gate fails once retention has dropped the head.
  • size() counts from the current head, so the offset was only right before the first drop.
  • The verification costs the store.

Change

The resume offset is the tape end (what retention dropped plus what the store holds; new FileStore::tape_start / tape_end). The check is the store's last 16 KiB against the file just before that offset, plus its first 16 KiB while it still has its head. The join is decided by the bytes, so a half-imported last line resumes in the middle of the line, and a retention-trimmed store resumes like any other.

A source that begins where the store begins still says it is the same file, so for that source a mismatch or a shorter file is refused, with the old wording. Anything else that fails the comparison falls through to the per-source handling exactly as before, so no store behaves worse than it did. --quick is accepted and ignored; the unit files that pass it keep working.

Checked against the old binary

A store of 187 chunks, retention left 14, then the source grew by 200 lines:

result
old exit 1: …predates everything in s.log (starts 2026-10-03 13:32:47.000) — import in chronological order, or to a new target
new exit 0: imported 200 lines … after 766890 bytes already imported

On a 124 MB store, re-importing 1000 new lines: 0.07 s old, 0.01 s new, with the store byte-identical to the file either way and the same peak RSS (the old check went chunk by chunk). The old cost is proportional to the store and I only measured one size; I make no claim about larger ones. The two strings the VM test asserts (already up to date, imported 1 lines) are unchanged.

Tests

A grown file adds only the growth; a retention-trimmed store resumes; a line imported in part is completed in place, byte for byte; a rewrite that begins the same is refused; a truncated file is refused; a different file after the store is appended and not mistaken for a regrowth; a file whose start changed with the join intact is refused. Each of head, tail and retention is covered by its own test: removing the head comparison, the tail comparison, or the dropped-bytes term fails exactly that test. (My first versions of the head and tail tests did not discriminate, because the store was no larger than the window; I only found that by running the mutations.)

Not in this layer

The hint file, the search for a store with no hint, handling a file with no stamp, and the fallback table for an ambiguous resume. For a source that claims to be the same file, a mismatch is still refused: replacing that with the plan's fallback is a later layer, and I did not want to swap a refusal for an unbounded overlap dedup here.

🤖 Generated with Claude Code

Resuming a grown file meant checking that the file's first stamp equalled the
store's first chunk's, seeking to size(), and decompressing every imported
chunk to compare it with the file (--quick: the first, middle and last). The
gate failed once retention had dropped the head, so the import was refused as
predating the store; size() counts from the current head, so the offset was
only right before the first drop; and the verification cost the store.

The offset is now the tape end, what retention dropped plus what the store
holds, and the check is the store's last 16 KiB against the file just before
that offset, plus its first 16 KiB while it still has its head. The join is
decided by the bytes, so a half-imported last line resumes in the middle of the
line and a store whose head is gone resumes like any other.

A source that begins where the store begins still says it is the same file, so
for that source a mismatch or a shorter file is refused as before; anything else
that fails the comparison falls through to the per-source handling, so no store
behaves worse than it did. --quick is accepted and ignored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@torstei
torstei merged commit bf8c380 into main Oct 3, 2026
7 checks passed
@torstei
torstei deleted the bugfix/import-resume-by-tail branch October 3, 2026 11:30
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.

1 participant