Sync both stores, with the exclusion lists they actually need - #110
Merged
Conversation
ADR-0007 split the data across two stores. The sync scripts still mirrored
one, so bars written to $MARKETDATA_STORE reached neither the Mac nor the
dash — a producer running green every night while half its output stayed on
the Windows box.
The fix is not "add a second pass with the same flags". The two stores
disagree about the one file that matters:
COT store manifests/<half>.json is live; root manifest.json is a dead
legacy aggregate, and excluding it is correct.
bar store root manifest.json is the ONLY index; there is no manifests/.
Both robocopy /XF and rsync --exclude match by NAME AT ANY DEPTH, so
carrying the COT list over to the bar store strips its whole index in
transit and delivers a replica full of parquet it cannot enumerate. Every
file is present, so size and a directory listing both look right; only a
read notices. That is the same trap this repo already documents one
directory over, for vintage/snapshots.json.
So: two passes, two lists, per transport.
sync-store.cmd two robocopy /MIR passes; pass 2 runs even when pass 1
fails, and the exit is the worse of the two codes
push-to-server.cmd two rsync pairs (data --delete, then the manifest on
its own without --delete, so the mirror cannot remove
the replica's copy before the new one lands)
pull-store.sh the same split on the consumer-pull side
verify-replicas.sh goes from two checks to four, and the two stores need
two different freshness signals — this is the part that would have been
wrong if copied across. cotdata rewrites status.json on EVERY run, so
"mtime is today" is a clean test. marketdata has no status.json and
rewrites manifest.json only when a bar is actually written, so a weekend,
a holiday or a deferred --require-final run legitimately writes nothing:
demanding "today" there would fail every Saturday, and a weekly false
alarm stops being read by the second month. Bars get a staleness window
instead (BAR_MAX_AGE_DAYS, default 4).
Both transports preserve timestamps, so a replica's mtime is the
PRODUCER's write time. That is what makes either test mean anything, and
it buys a sharper one for free: both replicas should carry identical
mtimes, so a mismatch means one push is behind even while both sit inside
the window and neither looks wrong alone.
sync_preflight.py learns both layouts. It did not fail loudly on a bar
store — it half-worked, which is worse: the manifest loaded through the
legacy fallback so the summary looked plausible, while the on-disk check
globbed a flat prices/ and never descended into bars/<domain>/<source>/,
reporting zero orphans no matter what a mirror would delete. Layout is now
detected per store, and being handed one of each exits 2 rather than
guessing — that pairing is far likelier to be two swapped paths than an
intention, and the mirror it would green-light deletes the destination.
Tests, because these are the files whose whole job is to notice:
test_sync_preflight.py 12 tests — layout detection, the nested-bars
orphan regression, cross-layout refusal
test_verify_replicas.py 8 tests — runs the real bash script with ssh
and both --check binaries stubbed on PATH
Also renamed the markers so none is a prefix of another
(REPLACE_WITH_LOCAL_COT was a prefix of REPLACE_WITH_LOCAL_COTDATA_STORE,
which quietly breaks a find-and-replace), and made verify-replicas.sh's
epoch formatting portable — `date -r` reads a number on BSD and a filename
on GNU.
219 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jo4iovRfc2fzE9MwcLp7r2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADR-0007 split the data across two stores. The sync scripts still mirrored one, so bars written to
$MARKETDATA_STOREreached neither the Mac nor the dash — a producer running green every night while half its output stayed on the Windows box.Why a second pass with the same flags would have been worse than nothing
The two stores disagree about the one file that matters:
manifests/<half>.jsonmanifest.json(root)manifest.jsonBoth
robocopy /XFandrsync --excludematch by name at any depth, so copying the COT store's exclusion list onto the bar store strips its whole index in transit and delivers a replica full of parquet it cannot enumerate. Every file is present, so disk usage and a directory listing both look right; only a read notices.This repo already documents that exact trap one directory over, for
vintage/snapshots.json. So: two passes, two lists, per transport.sync-store.cmd— tworobocopy /MIRpasses. Pass 2 runs even when pass 1 fails (the stores are independent; aborting early lets a COT hiccup silently stop bars), and the exit is the worse of the two codes.push-to-server.cmd— two rsync pairs: data with--delete, then the manifest on its own without--delete, so the mirror cannot remove the replica's copy in the window before the new one lands.pull-store.sh— the same split on the consumer-pull side.The freshness check needed two signals, not one
verify-replicas.shgoes from two checks to four. This is the part that would have been wrong if copied across:status.jsonon every run, new data or not → "mtime is today" is a clean test.status.jsonand rewritesmanifest.jsononly when a bar is actually written. A weekend, a holiday, or a deferred--require-finalrun legitimately writes nothing, so demanding "today" there fails every Saturday — and a weekly false alarm stops being read by the second month. Bars get a staleness window instead (BAR_MAX_AGE_DAYS, default 4: Friday's write is still fresh on Tuesday).Both transports preserve timestamps, so a replica's mtime is the producer's write time. That is what makes either test mean anything — and it buys a sharper one for free: both replicas should carry identical mtimes, so a mismatch means one push is behind even while both sit inside the window and neither looks wrong alone.
sync_preflight.pyhalf-worked on a bar store, which is worse than failingIt did not error. The manifest loaded through the legacy per-domain fallback, so the summary looked plausible — while the on-disk orphan check globbed a flat
prices/and never descended intobars/<domain>/<source>/, reporting zero orphans no matter what a mirror would delete.Layout is now detected per store. Being handed one of each exits 2 with
CANNOT JUDGErather than guessing: that pairing is far likelier to be two swapped paths than an intention, and the mirror it would green-light deletes the entire destination.Verification
These are the files whose whole job is to notice, so they now have tests:
tests/test_sync_preflight.py— 12 tests: layout detection, the nested-bars/orphan regression, cross-layout refusal, and the original checks unchanged.tests/test_verify_replicas.py— 8 tests running the real bash script withsshand both--checkbinaries stubbed on PATH. Covers the case that motivated this: a current COT store beside a stale bar store, green under the old two-check version and red under this one.219 passed, ruff clean. The
.cmdfiles could not be executed — there is nocmdin this sandbox — so their control flow is reasoned from cmd's parse-time expansion rules, same caveat as #109. What I checked rather than remembered: marketdata's store layout,raw_root(), and thatmanifest.jsonis written by_touch_manifestonly on an actual write, all read from the marketdata source.Also in here
REPLACE_WITH_LOCAL_COTwas a prefix ofREPLACE_WITH_LOCAL_COTDATA_STORE, which quietly corrupts a find-and-replace.verify-replicas.shepoch formatting made portable:date -rreads a number on BSD and a filename on GNU.Generated by Claude Code