synology_apm_repo.sdk.units.verify_reachable module¶
verify_reachable(): the top-down, reachability-scoped integrity
check — what Repository.verify()/Catalog.verify() (the CLI/TUI’s
own entry point) actually call. See ARCHITECTURE.md’s Dedup Layer
section for the FULL/QUICK validation-tier table this module implements.
Walks Catalog → Workload → Version → this version’s own composition
record(s) (via units.verify_extents.composition_extents_for_version),
and only checks data actually reachable that way — a stale/orphaned
file_map row nothing legitimate still references is never visited.
Per-workload-type extent resolution lives in units.verify_extents; the
shared bucket-check core and multiprocess worker glue live in
units.verify_bucket_check. This module holds the stateful walk itself
(_ReachabilityWalker) and the verify_reachable() entry point that
drives it.
Composition records get checked exhaustively; buckets get checked
whole. No row cap, no bucket-count cap — every touched
(stream_id, bucket_id) gets checked once no matter how many separate
chunk-map entries or versions touch it, and a claimed bucket’s
candidate chunks are every live (non-COMPACTED) physical chunk in
it, not just the specific chunk-map-referenced indices that touched it —
trading a small amount of extra dead-chunk checking for dropping all
per-chunk-index bookkeeping, which is what makes the bucket-level
batching and concurrency below practical.
FULL: every live chunk in a touched bucket gets both its ciphertext CRC32 and — once a vault key is available — its decrypt+decompress+ SHA-256 fingerprint checked, via one merged
chunk_walk.exec_chunkspass per bucket. As thorough, and as costly, as a real full export of the whole repository, not a bounded sample.QUICK: no chunk content is read or checked at all — only each touched bucket’s own structural checks (
check_bucket_structure(): file size vs.expected_bucket_size(), ChunkCrcStore trailer self-consistency, SizeStore-repair reporting) plus the header-derivedSymptom.KEY_MISSINGcheck. To check chunk content, use FULL — there is deliberately no sampled middle tier.
This module never checks whether a key is needed before running —
Repository.verify()/Catalog.verify() gate on
_require_key_verified() first, the same guard workloads()/
versions() already use. Without that gate this walk would see zero
versions for a confirmed-encrypted repository run without a key and
report a misleadingly clean result; a caller bypassing that gate is on
its own, though the per-bucket Symptom.KEY_MISSING handling below
still covers it.
Every catalog-listed, non-deleted version is attempted directly, the
same raw list Catalog.versions() itself now returns — a version this
walk can’t resolve reports a Finding(Stage.VERSION,
Symptom.DATA_MISSING, ...) here and raises the same way when actually
opened for browsing; the two are not two different views of the catalog.
GW/M365’s own resolution (units.saas.stream.SaasStream.open_saas_obj)
already forward-resolves past routine, backend-side generation rotation
before this walk ever sees a failure, so a GW/M365 Finding here means
a genuine gap, not the common case an earlier, less complete resolution
once made it look like.
Every Finding is tagged with the exact catalog/workload/version being
checked when it was found (Finding.ref) — for a bucket/chunk shared
by more than one version through dedup, ref names whichever
version’s check actually triggered the finding first, not every version
that touches the same data; _bucket_claim is the bucket-stage
tagging mechanism.
Scope, stated as what’s actually checked:
link.key↔repo_state.link_key(APV) consistency is not checked.Generation-selection auditing confirms that each supplemental DB (
copy_target_version,file_meta, …) this walk queries opens; it does not separately audit every other DB in the repository.A bucket physically present in
Pool/but not referenced by any live, resolvable version (a “dangling” bucket — the mirror image of a referenced-but-missing one, which does surface as aFinding) is never enumerated or reported: doing so needs a full recursive listing ofPool/’s own arbitrary-depth directory tree, a genuinely different, unmeasured-cost operation from the catalog-driven walk this module performs.
- async synology_apm_repo.sdk.units.verify_reachable.verify_reachable(repo, level=VerifyLevel.QUICK, *, progress=None, executor=None)¶
Run the top-down, reachability-scoped check and return every
Findingfound — seeVerifyLevelfor whatlevelcontrols.progressis reported in two stages. While discovering (walking every version’s composition extents and claiming the buckets they touch),phase="discovering",determinate=True,unit="items"— one tick per(workload, version)pair, a stable denominator known up front. While checking (once every version has been discovered),phase="verifying",determinate=True— one tick per bucket actually checked,done/totalboth real, cumulative counts already fixed by the time checking starts. The unit (and so whatdone/totalactually count) depends onlevel:"bytes"at FULL (_total_bytes_to_verify, from each claimed bucket’s own real on-disk size),"buckets"at QUICK (a plain count) — seefinalize_pending_bucketsfor why bytes would overstate QUICK’s own real throughput rather than just being unavailable.Progress.detailnames whichever(workload, version)pair the bucket just finished was claimed while discovering.A
connections/workloads/versionscall failing (a corrupt or unreadableconnection_config/workload_config/copy_target_versionrow or table) is caught at whichever level it happened and folded into aFindingvia_unresolvable_finding, same as every other resolution failure this module reports — not left to propagate out of this function entirely (seeStage.VERSIONfor why one stage covers both this level and the one above it). A connection/workload whose own listing fails is simply skipped for the rest of this walk — its siblings still get checked.executor(defaultNone: this call builds its own, iff FULL’s own bucket sweep actually needs one, and closes it before returning — given: the caller already built one and owns its lifetime) is FULL level’s real multi-core parallelism for its per-bucket decode sweep — see_ReachabilityWalker.check_all_bucketsfor why. Pass a shared one when calling this more than once for one logical operation (Repository.verify()’s own multi-catalog fan-out does exactly this) rather than let each call spin up its own pool independently. QUICK never uses it regardless of what’s passed.