synology_apm_repo.sdk.asynccache module

AsyncKeyedCache — the one memoizing-cache shape this SDK keeps reinventing by hand.

Pool._buckets/._chunks (bounded LRU, locked, session-wide shared), dedup.pool.BucketReaderCache (unbounded by default, private to one bulk sweep so it doesn’t evict Pool’s own shared cache), storage.dircache.DirCache (unbounded, session-wide shared), dedup.composition_reader.CompositionRecord’s page cache (bounded LRU, scoped to one record), and units.verify_reachable._ReachabilityWalker ._composition_records (bounded LRU, one run’s worth of shared records) are all the exact same operation — check a dict, await a fetch on miss, store the result, optionally evict the oldest entry once over a cap — each written independently with its own, slightly different correctness properties (some lock, some don’t; some accept “two callers miss the same key at once and both pay for a redundant fetch” as a deliberately-accepted race, some don’t even consider it). This module factors that one operation out once, with one well-tested concurrency contract, so every caller only has to get its own fetch function right.

Deliberately at the SDK package root, not inside storage/ or dedup/: it has zero dependencies beyond the stdlib, and both of those packages need to depend on it (storage.dircache.DirCache`, several places under ``dedup/) — the same reason errors.py/identifiers.py live here instead of under a specific layer.

class synology_apm_repo.sdk.asynccache.AsyncKeyedCache(fetch=None, *, maxsize=None)

Bases: Mapping[K, V], Generic[K, V]

A key -> value cache backed by an async fetch callback — resolve is the fetch-and-store operation.

The Mapping interface (len(), in, iteration, .keys()/.items()/.values(), sync .get()) is a live, read-only snapshot for introspection and tests — it never triggers a fetch and never blocks.

maxsize (None = unbounded) is a plain mutable attribute, not fixed at construction — assigning a new value takes effect on the next resolve call past the new cap.

known_keys()

Every key with either a settled value or a fetch in flight right now, deduplicated — for a caller (e.g. Repository.close()) that needs to account for everything ever asked for, not just what .keys() (the Mapping interface, settled entries only) can see. Doesn’t trigger a fetch, doesn’t block; a key not yet asked for by the time this is taken is still invisible to it.

put(key, value)

Insert key -> value directly, no fetch involved — for a caller that already has a value in hand (e.g. decoded as a byproduct of a batched fetch elsewhere) and wants it remembered here too, without paying for the Future/in-flight-dedup machinery resolve() needs to await a real fetch on miss. Safe with no lock, the same reason invalidate() needs none: plain sync code can’t be preempted mid-call on asyncio’s single-threaded event loop. Overwrites an existing entry for key rather than leaving it — fine for every current caller, where the same key always maps to the same value.

async resolve(key, fetch=None)

Return the cached value for key, fetching and storing it first if this is a miss.

fetch overrides whatever was bound at construction for this one call. Passing neither is a caller bug: raises TypeError immediately, same as Python would for any other missing required argument.

In-flight de-duplication: if another caller is already fetching this exact key, this call awaits that caller’s own in-progress fetch instead of starting a second, redundant one — both callers get the same value (or exception) for the cost of one fetch. The first caller to miss (the “owner”) is the only one that actually calls fetch; later callers become waiters, never seeing their own fetch argument even if one was given.

invalidate(key=None)

Drop key (or every cached entry, if key is None).

Also discards the result of any fetch already in flight for the affected key(s): without this, a resolve() that started before this call could still land its now-stale result in the cache afterward, silently undoing the invalidation.

async settle_all()

Resolve every key known_keys() reports right now — settled or still fetching — for a caller that needs the whole cache quiesced before it does something that depends on nothing still being in flight (Repository.close()/set_key() both drain this way before touching every open DedupRepo, since a dict(self._store)/.values() snapshot only sees entries already settled: a fetch started by a concurrent caller — racing close()/set_key() itself — would otherwise stay invisible to either method’s own cleanup, and (for set_key() specifically) go on to land in the cache after the fact, permanently pinned to whatever key was active when it started. A key nobody has asked for yet at the moment known_keys() is taken is an unavoidably narrower, residual race neither method can retroactively account for — it’s racing the drain itself, not something already in flight when the drain began.

Returns the values that resolved successfully, keyed the same as known_keys(), plus the exceptions raised by every key that didn’t — a caller with nothing further to do for a failed key (set_key()’s own drain) can simply discard the second element.