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 -> valuecache backed by an asyncfetchcallback —resolveis the fetch-and-store operation.The
Mappinginterface (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 nextresolvecall 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()(theMappinginterface, 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->valuedirectly, 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 theFuture/in-flight-dedup machineryresolve()needs to await a real fetch on miss. Safe with no lock, the same reasoninvalidate()needs none: plain sync code can’t be preempted mid-call on asyncio’s single-threaded event loop. Overwrites an existing entry forkeyrather 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.fetchoverrides whatever was bound at construction for this one call. Passing neither is a caller bug: raisesTypeErrorimmediately, 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 callsfetch; later callers become waiters, never seeing their ownfetchargument even if one was given.
- invalidate(key=None)¶
Drop
key(or every cached entry, ifkeyisNone).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 openDedupRepo, since adict(self._store)/.values()snapshot only sees entries already settled: a fetch started by a concurrent caller — racingclose()/set_key()itself — would otherwise stay invisible to either method’s own cleanup, and (forset_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 momentknown_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.