exlab_wizard.cache.sync_state_writer#
Orchestrator-only writer for sync_state.json.
Operator-free per-file NAS sync design (2026-05-21). The orchestrator
writes one sync_state.json per run pending NAS sync, under
<run>/.exlab-wizard/sync_state.json. It is a freely-mutable
current-state map: per-file records are overwritten in place as a file
is synced, re-modified, and re-synced, and cleared_at is stamped once
when the run’s staging copy is cleaned up.
Disk-side guarantees follow the §4.4.5 CacheWriter contract:
msgspec.jsonfor typed encode/decode (schema validation in one pass).filelock.FileLockadvisory exclusive lock around every read-mutate-write cycle so concurrent updates never lose a record.Atomic write via tempfile +
fsync+os.replace(atomic_write_bytes()).
The run-level SYNCING / SYNCED / CLEARED rollup is derived on
read by SyncStateWriter.rollup_state() – it is never persisted,
because it can oscillate (a SYNCED run whose file is modified again
returns to SYNCING).
Classes
Writer for |
- class exlab_wizard.cache.sync_state_writer.SyncStateWriter[source]#
Bases:
objectWriter for
sync_state.json. Orchestrator-mode only.All public methods are
asyncto match the §4.4.5CacheWritercontract; the blocking lock + I/O work is dispatched throughasyncio.to_threadso the FastAPI event loop is never blocked.- async mark_cleared(run_path)[source]#
Stamp
cleared_atwith the current UTC time.Called once the run’s staging copy has been cleaned up; this flips the derived rollup to
CLEARED.- Parameters:
run_path (
Path)- Return type:
- mark_cleared_sync(run_path)[source]#
Blocking variant of
mark_cleared()for synchronous callers.Used by the synchronous
clear_run_dir()operator-clear path so an operator “Clear” stampscleared_atexactly like the automatic cleanup reaper does. No-op-safe whensync_state.jsonis absent – a record is created carrying onlycleared_at.- Parameters:
run_path (
Path)- Return type:
- async read(run_path)[source]#
Read and decode the run’s
sync_state.json.Returns a fresh empty
SyncStateJsonwhen the file does not exist yet – a run with no sync activity simply has no record. RaisesSchemaMajorMismatchError(§11.9.2) when the on-disk file carries a different schema major thanSYNC_STATE_JSON_VERSION.- Parameters:
run_path (
Path)- Return type:
- read_sync(run_path)[source]#
Blocking variant of
read()for synchronous callers.read()dispatches the blocking lock + decode throughasyncio.to_thread. This variant runs the lock + decode inline; synchronous read-side code – notablyexlab_wizard.orchestrator.staging_query.list_staged_runs()andexlab_wizard.orchestrator.staging_clear.clear_run_dir()– calls it directly. Some of those call sites run inside a@ui.pagehandler (i.e. on the event loop); the lock + a single small JSON decode is brief enough not to matter there, and the read-side query is itself synchronous, so there is noto_threadhop to make. Returns a fresh emptySyncStateJsonwhen the file is absent.- Parameters:
run_path (
Path)- Return type:
- static rollup_state(state)[source]#
Derive the run-level
RunSyncStaterollup fromstate.Pure function – no I/O, no mutation. The rollup is computed on read rather than persisted because the
SYNCING/SYNCEDdistinction oscillates as files are re-modified.CLEARED–cleared_atis set (takes precedence even if some files are unverified, e.g.keep_localfiles left on disk).SYNCED–filesis non-empty and every record has a non-nullverified_at.SYNCING– otherwise (no files tracked yet, or at least one file still unverified).
- Parameters:
state (
SyncStateJson)- Return type:
RunSyncState
- async set_keep_local(run_path, rel_path, value)[source]#
Toggle a file’s
keep_localflag, creating the record if absent.- Parameters:
- Return type:
- async upsert_file(run_path, rel_path, *, synced_signature=None, verified_at=None, verified_sha256=None)[source]#
Create or update one file record under an exclusive lock.
The record for
rel_path(a run-relative POSIX path) is created if absent, otherwise updated in place.keep_localis preserved by this method (toggle it viaset_keep_local()).The three remaining kwargs have asymmetric None handling by design:
synced_signatureandverified_atare always overwritten – even withNone. The poller’s re-modified-file flow relies on this: it callsupsert_file(... synced_signature=None, verified_at=None)to drop the verify marks on a file whose local bytes have changed, which flips the run-level rollup fromSYNCEDback toSYNCING.verified_sha256is preserved whenNoneis passed – the audit-trail digest survives re-verify passes that carry no fresh local SHA (notably the operator-triggeredforce_verifypath).