Skip to content

Conversation

@jdoucett
Copy link
Contributor

RFC: incremental, split-source Fortress archiving — design only, no engine changes

The root-cause counterpart to florasense-tools #56. #56 stops spurious depot-tree churn (no-op runs re-writing files); this is the fix for the actual cost driverarchive.py re-copies an entire experiment whenever anything in it changes.

Full doc: docs/RFC_incremental_archive.md.

TL;DR of the proposal:

  • 4A — split immutable bulk from volatile metadata. Imagery/omics are write-once → archive once, verify, never re-send. Small XLSX/JSON/txt → cheap cadence, and much is already backed by the git metadata repo. Kills "small file forces big re-copy" structurally.
  • 4B — manifest-diff incremental. Vault-side manifest.json + content-hash diff → delta tars of only new/changed files. Reuses the per-file MD5 archive.py already computes (and/or fs-checksum). Delivers the "overlap detection" + "vault-side manifest" gaps already on the list. Content-hash, never mtime.
  • 4C — manifest-driven restore + periodic consolidation to bound delta count (the only place a full re-copy happens, on a deliberate cadence).

Phased, each independently shippable + reversible:

  • 1a — measure-only: maintain the manifest, log "would skip N / re-archive M", still full-copy. Zero behavior change; lets us measure real redundancy first. Lowest risk — recommended starting point.
  • 1b — delta archiving (after 1a numbers + a real-depot restore drill).
  • 1c — volatile/immutable split config (lead with one experiment).

Value-blind and fail-safe (a lost/corrupt manifest ⇒ treat everything as new = full archive, never a silent skip).

Ask: sign-off on the phased approach + the open questions in §6 — especially whether to kick off with Phase 1a (measure-only).

🤖 Generated with Claude Code

Design-only (no engine changes). The root-cause counterpart to
florasense-tools #56: that PR stops spurious depot-tree churn; this addresses
the actual cost driver — archive.py re-copies an entire experiment whenever
anything in it changes.

Proposes: (4A) split write-once-immutable bulk from volatile small files so a
metadata edit never re-copies imagery/omics; (4B) a vault-side manifest +
content-hash diff + delta tars (reusing the per-file MD5 the engine already
computes and/or fs-checksum), which also delivers the "overlap detection" and
"vault-side manifest" items already on the gaps list; (4C) manifest-driven
restore + periodic consolidation to bound delta count. Phased: 1a measure-only
(no behavior change) → 1b delta archiving → 1c split config. Value-blind,
fail-safe (lost manifest ⇒ full archive, never silent skip). Open questions
flagged for sign-off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jun 24, 2026
…nged

Phase 1a of incremental archiving (PR #4 plan). The repository backup re-zipped
and re-htar'd every target on every run; a target whose live source is the same
set as its last SUCCESSFUL archive now skips — writing NO new tar and NO new run
log, so the prior success log stays the canonical record reclaim/coverage read.

Mechanism: before any preflight, archive.py finds the prior success log via
skippable_prior_log and checks the live source against it with source_matches_log
using EXACTLY reclaim.verify's no-drift conditions — same file count, nothing
modified after the archive ran, same total bytes, same file_pattern, same
source_folder — reusing reclaim.scan_source / run_datetime so the skip decision
can never disagree with reclaim's notion of "still archived". One metadata-only
walk; reads no contents.

Config flag `skip_unchanged` (default false): the unchanged/changed decision is
always computed and logged (free measurement of redundancy), but only ACTED on
(skip) when enabled. Bypassed by --fresh / --force-resume. No run-log schema
change; downstream reclaim/coverage untouched.

Hardened per an adversarial review:
- skippable_prior_log selects the latest success by parsed run_timestamp (NOT
  log-file mtime), agreeing with coverage.classify and immune to out-of-order
  mtimes (FS migration / cp -p), and returns it ONLY IF it is the newest run
  overall — a newer running/failed run (crash/retry in flight) blocks the skip.
- file_pattern / source_folder scope changes force a re-archive.
- Documented the accepted limitation: the stat-only triple can't see an
  equal-length, mtime-preserved in-place rewrite; bit-level integrity of archived
  data is fs-checksum-sweep's job (#60), the backstop for that pathological case.

Tests (stdlib unittest, suite 37→52 green): skippable_prior_log (newest success;
newer failure/running blocks skip; run_timestamp-not-mtime ordering; prefix
sibling; no-success); source_matches_log (unchanged match; count/byte/newer-mtime
/pattern-change/source-folder-change/missing-source/unparseable-ts → no match);
end-to-end subprocess test asserting a skip exits 0 and writes NO new run log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jun 25, 2026
Reverts 5df25c6. The in-engine incremental approach (skip-unchanged
Phase 1a + the PR #4 RFC) is superseded by Fortress-native incremental/
versioned backup, so the skip_unchanged path and its tests are removed
rather than carried as dead, default-off code.

No change to the default daily flow (skip_unchanged was default false).
Removes archive.py's skippable_prior_log / source_matches_log helpers and
the skip-decision block, plus tests/test_skip_unchanged.py. Remaining
suite: 37 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jdoucett
Copy link
Contributor Author

Closing as superseded: the in-engine incremental/split-source approach in this RFC is replaced by Fortress-native incremental/versioned backup. The skip-unchanged Phase 1a code (#5) is being reverted in the companion PR. Rationale and the waste measurements that informed the decision are retained outside the repo.

@jdoucett Jarrod S Doucette (jdoucett) deleted the docs/incremental-archive-rfc branch June 25, 2026 17:10
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jun 26, 2026
…ted Path B)

Authoritative buildable plan superseding the closed PR #4 RFC and the reverted
PR #5 skip-unchanged. Recommends Path A (zip-delta, keep htar_large) now with
Path B (GNU tar --listed-incremental + hsi put) deferred behind a Negishi spike.
Covers level decision (cheap mtime+size diff), BLONDE + star-topology manifest
(reclaim-ready), restore reassembly, leveled reclaim (verify_chain, required not
deferred), the accepted same-size/same-mtime in-place-rewrite limitation, and a
phased PR plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jarrod S Doucette (jdoucett) added a commit that referenced this pull request Jul 8, 2026
Wires the slice-1 primitives into an opt-in size-routing archive path. Default-off,
so an unset config is byte-for-byte the whole-target behavior (full suite 172 -> 192).

Config (load_config): size_routing (bool), t_small (int, floored 100 MB), shard_count
(null=derive), shard_target. DURABILITY GUARD (RFC §2.4): when size_routing is on,
refuse a log_dir under $HOME / scratch / the ~/globus_archive_tmp default — the
per-target manifest at {log_dir}/_vault/ must be shared+durable or reclaim can't find
it (routing_log_dir_ok). config.example.json annotated.

Engine:
- enumerate_source_catalog_excluded: one walk -> (survivor catalog, exclusion report),
  the routing data source (exclusion applied once, before partitioning).
- route_and_ship (LOCAL): partition survivors -> objects; skip-unchanged (the append
  win + resume); ship changed/new objects via the SAME make_zip_files ->
  send_to_fortress chain (only_arcnames per object, exclude_spec=None); K frozen from
  the manifest after baseline; drop deleted slots; manifest written atomically AFTER
  EACH shipped object (crash-safe resume). object_project_name = per-object stem
  (invariant #4).
- __main__: refuse size_routing + --globus up front (RFC §2.8); size-routing branch
  replaces whole-target Step 1/2 and emails a [SUCCESS] summary.

Tests: tests/test_routing_wiring.py (20) — durability guard, config validation,
survivor enumerator (incl. exclusion drop), baseline/append/solo-change/shard-change/
delete/K-frozen/--fresh/resume-after-crash, exclusion recorded in manifest.

See docs/RFC_incremental_v2.md §2.5 (slice 2). Aggregate reclaim (slice 3) is REQUIRED
before a routed target is reclaimed, else verify() false-DRIFTs each object log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign in to join this conversation on GitHub.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant