synology_apm_repo.sdk.dedup.pool package¶
Chunk pool: resolves a ChunkAddress to plaintext bytes.
The only place chunk decrypt+decompress happens. Three collaborating
classes: BucketReader (_bucket_reader.py — one .buk file’s
header/SizeStore/locators plus the actual per-chunk read), Pool (this
module — repository-wide entry point — resolves (streamID, bucketID)
to the right .buk path and caches both BucketReaders and
decoded plaintext chunks), and BucketReaderCache (_cache.py — the
private cache shape a bulk sweep needs instead of Pool’s own
session-wide bounded one — unbounded by default, since export’s own
repeat-visit pattern needs it, but a caller whose own access pattern
doesn’t benefit from unbounded growth passes its own maxsize).
- synology_apm_repo.sdk.dedup.pool.DEFAULT_BUCKET_CACHE_SIZE = 16¶
Pool’s own defaultbucket_cache_size– also the value everyBucketReaderCache(maxsize=...)construction site outside this module (chunk_walk.py,export_scheduler.py,units.content.pcps_disk) reuses for its own default bound, so those three stay tied to this one number instead of each separately hardcoding a copy that could silently drift from it.
- synology_apm_repo.sdk.dedup.pool.DEFAULT_CHUNK_CACHE_SIZE = 4096¶
Pool’s own defaultchunk_cache_size– also reused bydedup.repository.DedupRepo’s own matching default, so the two stay tied to this one number instead of each separately hardcoding a copy that could silently drift from it.
- synology_apm_repo.sdk.dedup.pool.INTERACTIVE_BUCKET_CACHE_SIZE = 64¶
bucket_cache_sizefor the onePoolevery non-bulk consumer of a repository’s own catalog shares (api.repository.Repository. _open_catalog_resources, reached by both TUI/SDK browsing and one-shot CLI commands likels/tree/doctorviaRepository.catalogs()) – bigger thanDEFAULT_BUCKET_CACHE_SIZEbecause a single real directory listing under a PC/VM device’s disk image can touch several dozen distinct.bukfiles at once, whichDEFAULT_BUCKET_CACHE_SIZE’s 16 slots can’t hold without evicting and later re-opening some of them mid-listing. Deliberately a new constant rather than raisingDEFAULT_BUCKET_CACHE_SIZEitself, which bulk-export’s ownBucketReaderCaches are tuned to for a different access pattern (each bucket touched once, sequentially across a whole sweep) that gains nothing from a bigger bound the way one deep tree walk does.
- class synology_apm_repo.sdk.dedup.pool.BucketReader(store, path, header, entries, vault_key, *, default_verify_ciphertext_crc=False, sizestore_repaired=False, known_file_size=None)¶
Bases:
objectentries/locators(lazySequences) are for external, sparse-access callers that only ever touch a handful of a bucket’s chunks (verify_checks.py’s spot-checks, thedumpCLI command). This class’s own per-chunk reads (read_chunk,read_chunks) go straight to the rawarray.arrayvaluesraw_chunk_arraysreturns instead, skipping per-chunkSizeStoreEntry/ChunkLocatorconstruction entirely — a bucket-major export touches nearly every chunk in nearly every bucket it opens, dense enough that even lazy construction adds up.- async classmethod open(store, path, *, vault_key=None, verify_ciphertext_crc=False)¶
Open
pathand parse its header + SizeStore in one read of up toCOMPRESS_RESERVED_LENG(16384) bytes.SizeStore’s CRC is always verified here (this is not configurable to skip — you cannot compute chunk locators without first reading the SizeStore bytes, so validating them is free at that point, unlike the composition layer’s much more expensive full-map-array
mapCrc). A CRC mismatch is not immediately fatal, though: this first tries the bucket’s own trailing Redundancy blob (see_attempt_size_store_repair) — every current-format bucket carries one (FORMAT-SPEC.md: ChunkCrcStore & Redundancy) — since this check does run on every open, unconditionally, this is the one place in the whole SDK where parity self-repair is genuinely transparent for every caller, not just an explicitverify/diagnosticscheck. A successful repair sets the returned reader’s ownsizestore_repaired— this method itself has noFinding-returning contract, soverify_checks. check_bucket_structureis what actually reports it.verify_ciphertext_crcbecomes this reader’s ownread_chunk/read_chunksdefault — it does not itself trigger reading the ChunkCrcStore trailer here.
- async ensure_chunk_crc_store()¶
Lazily read and self-validate this bucket’s ChunkCrcStore trailer (FORMAT-SPEC.md: ChunkCrcStore), caching the per-chunk ciphertext CRC32 values for
read_chunk/read_chunks’s ownverify_ciphertext_crcoption — and for a caller doing its own one-off structural self-consistency check — to share without a second read of the same bytes.- Raises:
FormatError – The trailer is shorter than declared.
DataCorruptError – The trailer’s own bytes don’t match the header’s
crcOfChunkCrcself-consistency field.
- async verify_chunk_ciphertext_crc(chunk_idx)¶
Check chunk
chunk_idx’s stored bytes against itsChunkCrcStoreentry, with no decrypt/decompress attempt at all — unlikeread_chunk’s ownverify_ciphertext_crc=option (which still goes on to decrypt afterward, and so still needs a vault key for an encrypted bucket), this works whether or not a key is even available. For a caller (verify) that wants this one check in isolation, independent ofKeyRequiredError.- Raises:
DataCorruptError – The chunk’s ciphertext doesn’t match its
ChunkCrcStoreentry.
- async verify_raw_chunk_ciphertext_crc(chunk_idx, raw)¶
The second half of
verify_chunk_ciphertext_crc, split out for a caller that already haschunk_idx’s stored bytes in hand — e.g. from a batchedread_raw_chunkscall — instead of fetching them itself.- Raises:
DataCorruptError –
rawdoesn’t matchchunk_idx’s ownChunkCrcStoreentry.
- non_compacted_chunk_indices()¶
Every chunk index with real, readable data — skips
COMPACTEDslots (reclaimed, not corruption). Built from the raw compress-type array, the same sourceread_chunk/read_chunksuse for their own per-chunk decoding, so a whole-bucket sweep costs one O(chunk_num) pass over plain ints with no per-chunkSizeStoreEntryconstruction — unlike a caller wanting just one representative chunk instead, which should indexentriesdirectly rather than call this for a single lookup.
- async read_chunk(chunk_idx, addr, *, verify_ciphertext_crc=None)¶
Decrypt (if the bucket is vault-encrypted) and decompress chunk
chunk_idx, returning exactly 4096 bytes of plaintext.addrsupplies the IV for decryption — pass the chunk’s ownChunkAddress.Reads straight off the raw arrays rather than
entries/locators— no reason for even a single cold-pathSizeStoreEntry/ChunkLocatorconstruction once the raw arrays already exist from__init__.verify_ciphertext_crc(None: defer to whatever this reader wasopen()ed with) compares this chunk’s stored bytes against itsChunkCrcStoreentry (FORMAT-SPEC.md: ChunkCrcStore) before decrypting — a general data-integrity check independent of encryption and needing no vault key, off by default since it costs an extra trailer read the first time it’s used per bucket.
- async decode_raw_chunk(chunk_idx, addr, raw, *, verify_ciphertext_crc=None)¶
The decrypt/decompress half of
read_chunk, split out for a caller that already haschunk_idx’s stored bytes in hand — e.g. from a batchedread_raw_chunkscall — instead of fetching them itself, the same splitverify_raw_chunk_ciphertext_crcalready has fromverify_chunk_ciphertext_crc.verify_ciphertext_crc: same meaning asread_chunk’s own parameter. A caller that already ranverify_raw_chunk_ciphertext_crconrawitself should passFalsehere — this would otherwise redo (and raise past) that same check.
- async read_raw_chunk(chunk_idx)¶
Fetch chunk
chunk_idx’s stored bytes exactly as written — compressed and/or encrypted permode, no decrypt/decompress applied. Forverify_checks.py’s ChunkCrcStore spot-check (FORMAT-SPEC.md: ChunkCrcStore), which checks the ciphertext CRC32 directly and has no need for the plaintextread_chunkproduces.- Raises:
ChunkCompactedError –
chunk_idxisCOMPACTED— checked first (likeread_chunk’s own_resolve_compress_typecall), since a compacted chunk has no ChunkCrcStore entry of its own; a caller resolving one anyway would otherwise index one past the end of that trailer’s own array.
- async read_raw_chunks(chunk_indices)¶
Batch form of
read_raw_chunk: mergeschunk_indices’ own locator byte-ranges that are within_GAP_TOLERANCEof each other into a singleObjectStore.readcall instead of issuing one read per chunk — the same run-mergingread_chunksdoes, minus the decrypt/decompress step neither this method nor the single-chunkread_raw_chunkperforms. Collapsing the read count like this matters most on a remote-object-storage backend, where every separate read is its own network round trip.- Parameters:
chunk_indices (Sequence[int]) – Must already be sorted ascending and deduplicated — same contract as
read_chunks’requests.- Raises:
ChunkCompactedError – For the first
COMPACTEDchunk found inchunk_indices, raised up front before any read happens — same asread_chunks.
- async read_chunks(requests, *, semaphore=None, verify_ciphertext_crc=None)¶
Batch form of
read_chunk: mergesrequests’ locator byte-ranges that are within_GAP_TOLERANCEof each other into a singleObjectStore.readcall instead of issuing one read per chunk, then decrypts/decompresses each chunk out of its own slice of whichever merged buffer it landed in. Decrypt stays strictly per-chunk; decompress is batched across a whole merged run instead, via_decode_run/decompress_many.- Parameters:
requests (Sequence[tuple[int, ChunkAddress | None]]) – Must already be sorted by
chunk_idxascending and deduplicated — not re-sorted or re-deduplicated here, so a caller bug in either regard surfaces as a wrong/inefficient merge rather than being silently hidden. Eachaddrmay beNonewhen the caller already knows this bucket isn’t vault-encrypted, since_decode_runnever dereferences it unless it is; a caller that doesn’t know ahead of time may still pass a real one.semaphore (Semaphore | None) – The shared concurrency pool for this whole export (default
None: serial) — the sameasyncio.Semaphoreexec_chunks()’s cross-bucket dispatch loop also draws from, so total concurrently in-flight reads across both levels never exceed one shared bound. A caller passing a non-Nonesemaphore has already acquired one permit on this call’s behalf; this method’s first merged run spends that permit directly, and only a second/third/… run (a bucket whose requested chunks land in more than one physically-separate region of its.bukfile) acquires its own additional permit from the same pool.verify_ciphertext_crc (bool | None) – Same meaning as
read_chunk’s own parameter (Nonedefers to whatever this reader wasopen()ed with) — checked per chunk, before decrypting.
- Returns:
A chunk’s value may be a
memoryviewrather than an independentbytesobject — it can view into eitherdecompress_many’s shared per-run decode buffer (ZSTD) or this method’s own merged I/O-read buffer (unencryptedNONE/LZ4), never copied just to satisfy this method’s own contract. Every consumer today only reads the bytes forward into another buffer, never holding the value past its own call.- Raises:
ChunkCompactedError – For the first COMPACTED chunk found in
requests, raised up front before any read happens.- Return type:
dict[int, bytes | memoryview]
- class synology_apm_repo.sdk.dedup.pool.BucketReaderCache(maxsize=None)¶
Bases:
objectExport’s bucket-major path and
verify’s Bucket-and-key stage each build one instead of going throughPool’s own bounded_buckets, so a sweep touching every bucket once doesn’t evict genuinely-hot interactive entries from that shared cache. Reads/writes never cross over withPool’s own cache — the two stay fully independent.maxsize(None, the default: unbounded) is bounded by however many distinct buckets one sweep actually touches unless the caller passes a smaller cap — see each construction site’s own reasoning for why it picked the value it did. Built onAsyncKeyedCachelike every other cache in this SDK, withfetchsupplied per call (typicallyPool.open_bucket_uncached) since this class is constructed at layers with noPoolreference of their own.One instance is shared across every fragment of a
VirtualDiskContentSourceexport, so a bucket one fragment opens stays open for the next; a standaloneexport_tocall or averifyrun instead each get their own separate instance.Note
Deliberately does not also cache decoded chunk plaintext across calls — same-call repeats are already deduplicated by
exec_chunksitself, and unconditionally remembering every decoded chunk would cost memory roughly equal to the whole export’s unique DATA content for little cross-call reuse against real VM-image exports. Chunk decode stays scoped to one bucket-group call, which is also what letsdecompress_manyhand back zero-copymemoryviewvalues instead of an independentbytescopy per chunk.- buckets: AsyncKeyedCache[tuple[StreamId, BucketId], BucketReader]¶
- class synology_apm_repo.sdk.dedup.pool.Pool(store, pool_root, dir_cache, *, vault_key=None, bucket_cache_size=16, chunk_cache_size=4096, verify_fingerprint=False, verify_ciphertext_crc=False)¶
Bases:
objectRepository-wide chunk pool entry point.
Resolves any
ChunkAddressto its 4096-byte plaintext, with two-level LRU caching:BucketReaders (header + locators, cheap, kept many) and decoded plaintext chunks (more expensive, kept fewer). Both areAsyncKeyedCacheinstances — session-wide, shared across every interactive caller — which already gives concurrency-safety (an internal lock, held only around bookkeeping, never around the I/O of opening a bucket or reading/decrypting/decompressing a chunk) and in-flight fetch de-duplication (two concurrent callers missing the same key pay for one fetch, not two) for free.backfill_chunkis the one direct-insert exception to that de-duplication guarantee.- release_caches()¶
Drop everything this pool holds in memory: decoded chunks, open bucket readers, and allocation tables.
Closing a repository does not, on its own, free any of this — the caches live on the
Pool, which a caller can still be holding a reference to. Anything walking several repositories in one process (a smoke run, a TUI session browsing one after another) would otherwise keep every pool it ever opened fully populated. TheDirCacheis deliberately untouched: it belongs to the store, not to this pool.
- property release_epoch: int¶
Bumped once per
release_caches()call – a caller fetching a chunk’s plaintext some other way thanread_chunk()/resolve()(dedup_file.py’s_resolve_bucket_group) captures this before starting, then skipsbackfill_chunkif it’s changed by the time the fetch finishes.
- property store: ObjectStore¶
Needed by
dedup.pool_descriptor.PoolDescriptor.from_poolto describe an equivalentPoolfor a multiprocess worker to rebuild — the same reasondedup.repository.DedupRepoalready exposes its ownstore.
- property verify_fingerprint: bool¶
This
Pool’s own session-wide default — needed alongsidestore/pool_root/vault_keybydedup.pool_descriptor.PoolDescriptor.from_poolso a multiprocess worker’s own rebuiltPoolmirrors this one’s actual configuration instead of silently resetting it.
- async bucket_path(stream_id, bucket_id)¶
Resolve
(stream_id, bucket_id)to its physical.bukpath (including any.<seqId>generation suffix), relative to the repository root.
- async bucket(stream_id, bucket_id)¶
Return the (cached)
BucketReaderfor(stream_id, bucket_id), opening and caching it on first access.
- async open_bucket_uncached(stream_id, bucket_id)¶
Open
(stream_id, bucket_id)fresh, without touching_bucketsat all — no lookup, no insert, no eviction.Exists for callers that need their own private, separately-scoped
BucketReadercache instead of this shared one (a bulk sweep likeexport_scheduler.export_to’s bucket-major path, orverify’s Bucket-and-key stage — so a sweep touching every bucket once doesn’t evict thisPool’s own genuinely-hot interactive entries)._bucketsitself is bound to call this for its own miss path (seePool.__init__); the two never diverge in how a bucket actually gets opened, only in whether the result is remembered in the shared cache afterward.
- async open_bucket_uncached_by_key(key)¶
open_bucket_uncached, taking its(stream_id, bucket_id)as one tuple — the single-arg shapeAsyncKeyedCache.resolve’sfetchcallback needs, so every caller building aBucketReadercache keyed this way (this class’s own_buckets,chunk_walk.py’s bucket-major export,verify’s Bucket stage) can pass this bound method directly instead of each writing its ownlambda key: pool.open_bucket_uncached(*key).
- async read_chunk(addr, *, cache=True, verify_fingerprint=None, verify_ciphertext_crc=None)¶
Resolve
addr— using its own embeddedstream_id/bucket_id, not any caller-assumed values — to 4096 bytes of plaintext. Not used by the bucket-major export scheduler (chunk_walk.py’s_exec_one_bucket_group), which callsBucketReader.read_chunksdirectly, bypassing this class entirely.cache=Falsebypasses the plaintext chunk cache entirely — for a one-off integrity check of a single chunk per bucket, which has nothing to gain from caching it and would only evict genuinely-hot interactive data for no benefit.verify_fingerprint(None: defer to whatever thisPoolwas constructed with; an explicitTrue/Falseoverrides it for this one call) compares this chunk’s plaintext SHA-256 against its stored.fgpfingerprint and raisesDataCorruptErroron a mismatch — a general data-integrity check independent of encryption, off by default since it costs one extra.inf/.fgpread per chunk. A plaintext-cache hit is checked too, not skipped, since the point is verifying the bytes about to be handed back.verify_ciphertext_crc— forwarded toBucketReader.read_chunkas-is (Nonedefers to whatever that reader was opened with). Checked before decrypting, so it still requires a vault key for an encrypted bucket to get as far as returning plaintext — a caller that wants the ChunkCrcStore check in isolation, independent of whether a key is even available, wantsBucketReader.verify_chunk_ciphertext_crcinstead.
- cached_chunk(addr)¶
addr’s already-decoded plaintext if it’s currently in_chunks, elseNone– never fetches, never blocks (backed byAsyncKeyedCache’s own read-onlyMapping.get). For a caller likededup_file.py’s multi-chunk batch path that wants to skip re-fetching a chunk another read already populated, without going throughread_chunk()’s own single-key fetch shape.
- backfill_chunk(addr, plain)¶
The write half of
cached_chunk’s peek: remembersaddr’s already-decodedplainbytes in_chunks, as if aread_chunk(addr)call had fetched them. Does not itself verify a fingerprint – the caller is responsible for that before backfilling.
- async verify_fingerprints(stream_id, bucket_id, chunks, *, verify_fingerprint=None)¶
Check every entry in
chunks(already-decoded plaintext, keyed by its ownchunk_idxwithin(stream_id, bucket_id)) against its stored.fgpdigest — the same policy/lookupread_chunkapplies to its own single chunk, factored out here soBucketReader.read_chunks’s multi-chunk batch result (fetched bydedup_file.py’s_fill_data_extentandchunk_walk.py’s_exec_one_bucket_group, both of which callread_chunksdirectly and so bypassread_chunkentirely) can honor it too, instead of silently skipping verification whenever a read/export spans more than one distinct chunk.verify_fingerprint— same meaning asread_chunk’s own parameter:Nonedefers to thisPool’s session-wide default.- Raises:
DataCorruptError – Any chunk’s plaintext SHA-256 doesn’t match its stored fingerprint.
- async fingerprints_for(stream_id, bucket_id, chunk_indices)¶
Batched
dedup.fingerprint.fingerprints(), using this Pool’s ownAllocationTableCache—verify_fingerprints’s own lookup half (resolves the stored digests only, no plaintext comparison of its own), factored out so its own.inf/.fgpresolution shares thisPool’s cache the same way every other lookup here does.