exlab_wizard.sync.nas_client#

NAS sync client. Backend Spec §7.1, §7.3.

The NASSyncClient is the public surface of the NAS sync subsystem. It wires together the durable queue, the transport drivers, the SHA-256 verifier, the bandwidth scheduler, the cleanup interlocks, and the Pre-Sync Gate.

Per §7.1 the client is an in-process module of the FastAPI app; there is no separate daemon. Workers are asyncio tasks; the queue file is the durable record so a server restart does not lose pending work.

Classes

NASSyncClient(*, config, queue_db, ...[, ...])

Durable, per-equipment NAS sync queue with Pre-Sync Gate.

SyncJobHandle(job_id, state, run_path[, ...])

Lightweight handle returned by NASSyncClient.enqueue().

class exlab_wizard.sync.nas_client.NASSyncClient(*, config, queue_db, validator, cache_creation, sync_state_writer=None, keyring_store=None, worker_poll_interval_s=0.05, push_callable_factory=None, check_callable_factory=None, lsjson_callable_factory=None)[source]#

Bases: object

Durable, per-equipment NAS sync queue with Pre-Sync Gate.

Backend Spec §7.1, §7.3.

Lifecycle:

  • init() opens the queue DB, replays any in-flight jobs, and starts a single background worker task.

  • enqueue() runs the Pre-Sync Gate, gates the run if needed, and otherwise inserts a QUEUED row.

  • close() cancels the worker and closes the DB.

The worker loop is a simple “pick the oldest QUEUED whose next_attempt_at has passed” scheduler with at-most-one inflight job at a time. This keeps determinism for tests; production deployments can extend to per-equipment parallelism without changing the public API.

Parameters:
apply_config(config)[source]#

Swap the cached config + equipment map in place (no relaunch).

The equipment_id -> EquipmentConfig lookup is rebuilt into a local before assignment so the worker loop – which runs in the same event loop – never observes a half-built map. In-flight queued jobs carry their own captured paths; a removed equipment id simply errors that one job exactly as it would after a relaunch.

Parameters:

config (Config)

Return type:

None

async close()[source]#

Stop the worker and close the queue DB. Idempotent.

Return type:

None

async enqueue(run_path, files=None)[source]#

Pre-Sync Gate -> if hard-tier finding without override, mark sync_status='blocked_by_validation'. Otherwise insert a QUEUED row.

files (operator-free per-file NAS sync, 2026-05-21) is the per-file subset of run-relative POSIX paths eligible at enqueue time; an empty / omitted list means “the whole run”.

The queue holds one row per run_path (UNIQUE). Re-enqueue behaviour:

  • existing job in a terminal state (VERIFIED / CLEANUP_ELIGIBLE / CLEANED / FAILED) and a non-empty files list -> reset to QUEUED carrying the new subset. This is how a file modified after a prior verify gets re-synced.

  • existing job in a terminal VERIFIED / CLEANUP_ELIGIBLE / CLEANED state with an empty files list -> falls through to a no-op: there is no subset to re-sync and a successfully-verified run is not blindly re-queued. (Only a terminal FAILED row with empty files is re-armed – the manual-retry branch below.)

  • existing job active (QUEUED / RUNNING / AWAITING_VERIFY) -> no-op (newly settled files ride the next sweep).

  • existing terminal FAILED job with no files -> re-armed via reset_to_queued so the manual-retry contract holds.

  • no existing job -> insert a QUEUED row with files.

Returns a SyncJobHandle. The handle’s state is either SyncHandleState.BLOCKED or SyncHandleState.QUEUED.

Parameters:
Return type:

SyncJobHandle

async force_verify(run_path)[source]#

Re-run rclone check --download against the configured remote.

Used by the Settings “verify integrity” action. Reports only – does NOT advance the queue state and does NOT update verified_sha256 in sync_state.json (the rclone-only migration deliberately keeps Slot A SHA capture scoped to the sync-time path that has access to a freshly-read local copy).

Resolves the equipment from run_path’s first component, gathers every tracked file in sync_state.json as the --files-from subset, and asks the driver to compare. Returns a populated VerifyResult; the caller renders mismatched, missing, and errors to the operator. A run with no tracked files yields ok=True (nothing to verify).

Parameters:

run_path (Path)

Return type:

VerifyResult

async init()[source]#

Open the queue and start the worker task. Backend Spec §7.1.2.

Return type:

None

async retry(job_id)[source]#

Re-arm a FAILED job. Backend Spec §7.1.5 (Problems-tab Retry).

Parameters:

job_id (str)

Return type:

None

async status(run_path)[source]#

Return the queue state of the job for run_path.

"none" when no job exists; otherwise the underlying SyncJobState value.

Parameters:

run_path (Path)

Return type:

str

class exlab_wizard.sync.nas_client.SyncJobHandle(job_id, state, run_path, blocking_findings=())[source]#

Bases: object

Lightweight handle returned by NASSyncClient.enqueue().

job_id is empty when the gate blocked enqueue (the on-disk sync_status will reflect the block). blocking_findings is present iff state == BLOCKED.

Parameters:
blocking_findings: tuple[Finding, ...]#
job_id: str#
run_path: str#
state: SyncHandleState#
class exlab_wizard.sync.nas_client.SyncJobState(*values)[source]#

Bases: StrEnum

State machine for a sync job. Backend Spec §7.1.2.

AWAITING_VERIFY = 'awaiting_verify'#
CLEANED = 'cleaned'#
CLEANUP_ELIGIBLE = 'cleanup_eligible'#
FAILED = 'failed'#
QUEUED = 'queued'#
RUNNING = 'running'#
VERIFIED = 'verified'#