-
SeriesCache — A
dictof resolved volume ids with an atomic first-writer claim.
comicbox.formats.base.online.series_cache¶
source module comicbox.formats.base.online.series_cache
Thread-safe series cache for series-first batching (plan ยง3.10).
Maps (source_name, series_fingerprint) to the resolved upstream volume/series id, so the second and later issues of a series skip the cold-path candidate search and ask the source directly for “issue N in volume V”.
The population rule is FIRST WRITER WINS: a falsy-group collision (two unrelated comics that happened to share a fingerprint) must never overwrite an already-resolved entry. Expressing that as if key in cache: return followed by cache[key] = value is fine for the sequential caller (OnlineSession.tag_many) but races once the CLI’s -j N thread pool drives it: two workers can both pass the membership test and both claim to be the first writer, emitting two SeriesIdentified events for one series.
SeriesCache closes that by making the claim a single locked operation. It stays a plain MutableMapping so callers that just want a dict โ and the tests that pass one โ keep working unchanged.
Classes
Functions
-
filename_series_fingerprint — Lightweight series fingerprint derived from the filename alone.
-
lead_series — Claim the right to resolve
key, on any MutableMapping. -
release_series — Hand leadership of
keyback, on any MutableMapping. -
await_series_leader — Block while another caller resolves
key, if one is. -
claim_series — First-writer-wins population that works on any MutableMapping.
source filename_series_fingerprint(path: Path) โ str
Lightweight series fingerprint derived from the filename alone.
Lets a batch dispatcher cluster same-series comics together before opening any archives, so the first issue of each cluster pays for the cold-path search and the rest read the resolved volume id back out of the cache. Both dispatchers use it: OnlineSession.tag_many and the CLI’s Runner.
Falls back to the bare filename when comicfn2dict can’t extract a series โ those files won’t benefit from batching but won’t break it either (they sort to a deterministic “by filename” bucket).
Deliberately mirrors _series_fingerprint’s component choice (see comicbox/box/online_lookup.py): the per-issue year is excluded, because clustering by it would scatter a multi-year run into one cluster per year and hand the cache a fresh cold path for each โ the opposite of what the clustering is for. The volume ordinal is kept, since that is what separates reboots.
source class SeriesCache()
Bases : MutableMapping[SeriesCacheKey, int]
A dict of resolved volume ids with an atomic first-writer claim.
Also arbitrates SINGLE-FLIGHT resolution of a series. Sorting a batch by series fingerprint makes the cache hit for the second and later issues of a run โ but only once the first one has finished, and a -j N pool starts the first N files of a cluster at the same instant. All N then miss, and all N pay for the cold search: the batching saves nothing for exactly the files it was added for.
So one caller per key LEADS and the rest WAIT for it, then re-read the cache and take the cheap volume-scoped path. A leader that resolves nothing (no match, prompt declined, source failure) hands leadership to the next waiter rather than stranding the cluster.
Start empty.
Methods
-
claim — Populate
keyand return True only if this caller was the first. -
lead — Claim the right to resolve
key; True when this caller must do it. -
wait — Block until the current leader of
keyreleases, ortimeout. -
release — Finish leading
keyand wake everyone waiting on it. -
snapshot — Return a consistent copy. Useful for persistence.
source method SeriesCache.claim(key: SeriesCacheKey, volume_id: int) โ bool
Populate key and return True only if this caller was the first.
The membership test and the insert happen under one lock, so exactly one concurrent caller is told it won โ which is what the SeriesIdentified event and the first-writer-wins contract both rely on.
source method SeriesCache.lead(key: SeriesCacheKey) โ bool
Claim the right to resolve key; True when this caller must do it.
False means another caller is already resolving it and this one should wait. Resolved keys also return False โ there is nothing left to lead. A leader MUST call release in a finally, or its waiters hold until their timeout.
source method SeriesCache.wait(key: SeriesCacheKey, timeout: float) โ bool
Block until the current leader of key releases, or timeout.
Returns True when a leader released within the timeout. Either way the caller must re-read the cache: a leader can finish without resolving anything.
source method SeriesCache.release(key: SeriesCacheKey) โ None
Finish leading key and wake everyone waiting on it.
source method SeriesCache.snapshot() โ dict[SeriesCacheKey, int]
Return a consistent copy. Useful for persistence.
source lead_series(cache: MutableMapping[SeriesCacheKey, int], key: SeriesCacheKey) โ bool
Claim the right to resolve key, on any MutableMapping.
False for a mapping without the single-flight API โ a plain dict from a sequential caller, which has nothing to race. A True caller MUST pair this with release_series in a finally.
source release_series(cache: MutableMapping[SeriesCacheKey, int], key: SeriesCacheKey) โ None
Hand leadership of key back, on any MutableMapping.
source await_series_leader(cache: MutableMapping[SeriesCacheKey, int], key: SeriesCacheKey, timeout: float = SERIES_LEAD_TIMEOUT_S) โ None
Block while another caller resolves key, if one is.
Returns as soon as there is no leader to wait for, which is also the plain-dict case. The caller re-reads the cache either way: a leader can finish without having resolved anything.
source claim_series(cache: MutableMapping[SeriesCacheKey, int], key: SeriesCacheKey, volume_id: int) โ bool
First-writer-wins population that works on any MutableMapping.
Uses SeriesCache.claim when the mapping provides it (the concurrent case) and falls back to check-then-set for a plain dict, which is all a single-threaded caller needs.