Skip to content

fix(migrate): stop the .rtc migration from destroying outputs, and allow a read-only source - #139

Closed
bandrel wants to merge 3 commits into
masterfrom
feat/migrate-nas-source
Closed

bandrel wants to merge 3 commits into
masterfrom
feat/migrate-nas-source

Conversation

@bandrel

@bandrel bandrel commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Why

The NetNTLMv1 .rtc -> .rt.zst migration lost 32 outputs in production on 2026-07-30. Two driver processes ended up on range 3001-4095 after a relaunch. Worker A converted a part and deleted its .rtc; worker B, still holding that part in its file list, ran rtc2rt against the now-missing source, failed — and fail() deleted the good .rt.zst worker A had just written, because it removed zst_path unconditionally. Those parts ended up with neither a .rtc nor a .rt.zst, recoverable only because a complete second copy of the .rtc set exists on a NAS.

Using that backup as a source was itself unsafe: the script derived the output path from the source path and deleted each .rtc after conversion, so pointing --base-dir at the backup would have written outputs onto it and consumed it.

What

  • --dest-dir mirrors the source layout under a separate root and never deletes the source, so a read-only copy can be the source. Without it, the previous in-place behaviour is unchanged. Because the source is then immutable, pending_parts() also treats a non-empty output as done (a zero-length one means an interrupted write).
  • fail() no longer deletes an output it did not create — it records whether zst_path existed before this invocation started. Cleanup of its own partial output is unchanged.
  • driver_lock() takes an exclusive non-blocking flock on <manifest>.lock for the whole run, so a second driver on the same range exits with a clear message instead of racing. Ranges were already the isolation boundary between hosts, but nothing enforced one driver per range on a host.
  • The script and its first test suite are now tracked; until this PR they existed only on the migration hosts.

Verification

  • 26 tests, stdlib unittest (no pytest) so they run on the hosts' bare python3. Green on macOS and on all three migration hosts.
  • Mutation-checked: reintroducing the unconditional delete fails exactly 2 of them.
  • Byte-identical against the in-place path: all 4 parts of 0-1000/5 converted from the NAS source are cmp-identical to the .rt.zst the in-place pipeline had already produced for those parts, with the NAS sources intact and nothing written into the source tree.
  • Lock verified on real hardware: a second driver exits rc=1 with another driver already holds .../m.jsonl.lock.
  • Recovery run in production: 67/67 parts rebuilt from the NAS with zero errors; all 28 whose sources were then removed passed zstd -t first.
  • Local CI gates pass: make cpu-tests + ./crackalack_cpu_tests (all pass) and make macos.

🤖 Generated with Claude Code

bandrel and others added 3 commits July 30, 2026 21:51
The NetNTLMv1 .rtc -> .rt.zst migration derived its output path from the
source path and deleted each .rtc after a verified round trip. That is
correct when the source tree IS the working copy, but it made the script
unusable against the complete second copy of the .rtc set on the NAS:
pointing --base-dir at it would have written outputs onto the NAS and
deleted the backup.

Add --dest-dir, which mirrors the source layout relative to --base-dir
under a separate root and never deletes the source. This lets every worker
read .rtc from the NAS over its own 2.5 GbE link while the destination host
only receives .rt.zst, so it no longer serves source reads for the whole
fleet and can reclaim the space its .rtc copy occupies.

Because the source is immutable in this mode, a deleted .rtc can no longer
signal progress, so pending_parts() also treats a non-empty output as done
(a zero-length one means an interrupted write, not a finished part).

Verified on dell3 against the real binaries: all 4 parts of 0-1000/5
converted from the NAS source are byte-identical to the .rt.zst the
in-place pipeline already produced for those parts, all 4 NAS sources
survive, nothing was written into the NAS tree, and scratch is clean.

Also commits the script and its first test suite, which until now existed
only in dell3:~/projects and dell4:/tmp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On 2026-07-30 two drivers ended up running against range 3001-4095 after a
relaunch. Worker A converted a part and deleted its .rtc; worker B, whose file
list still contained that part, ran rtc2rt against the now-missing source,
failed, and fail() removed the good .rt.zst worker A had just written --
because it deleted zst_path unconditionally. 32 parts were left with neither a
.rtc nor a .rt.zst and had to be rebuilt from the NAS backup. Nothing was
permanently lost only because that backup exists.

Two fixes:

  - convert_one_part() now records whether zst_path existed before it started
    and only cleans it up on failure if this invocation created it. Cleanup of
    our own partial output is unchanged.

  - Add driver_lock(): an exclusive non-blocking flock on <manifest>.lock, held
    for the whole run, so a second driver on the same manifest (i.e. the same
    range) exits with a clear message instead of racing. Ranges were already the
    isolation boundary between hosts, but nothing enforced one driver per range
    on a single host.

Adds 7 tests. Verified they catch the real defect: reintroducing the
unconditional delete fails exactly 2 of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the two failure modes that actually cost data during the NetNTLMv1
campaign, so the next person does not rediscover them: --dest-dir is mandatory
when the source tree is a backup (without it the script deletes each .rtc), and
one driver per range is enforced by an flock on <manifest>.lock. Also notes that
the driver's stdout is block-buffered when redirected, so manifest lines rather
than log lines are the progress signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bandrel
bandrel deleted the branch master August 2, 2026 00:31
@bandrel bandrel closed this Aug 2, 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.

1 participant