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_chunks pass 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-derived Symptom.KEY_MISSING check. 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:

  1. link.key ↔ repo_state.link_key (APV) consistency is not checked.

  2. 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.

  3. 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 a Finding) is never enumerated or reported: doing so needs a full recursive listing of Pool/’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 Finding found — see VerifyLevel for what level controls.

progress is 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/total both real, cumulative counts already fixed by the time checking starts. The unit (and so what done/total actually count) depends on level: "bytes" at FULL (_total_bytes_to_verify, from each claimed bucket’s own real on-disk size), "buckets" at QUICK (a plain count) — see finalize_pending_buckets for why bytes would overstate QUICK’s own real throughput rather than just being unavailable. Progress.detail names whichever (workload, version) pair the bucket just finished was claimed while discovering.

A connections/workloads/versions call failing (a corrupt or unreadable connection_config/workload_config/ copy_target_version row or table) is caught at whichever level it happened and folded into a Finding via _unresolvable_finding, same as every other resolution failure this module reports — not left to propagate out of this function entirely (see Stage.VERSION for 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 (default None: 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_buckets for 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.