yggdrasil.dataclasses.expiring¶
expiring ¶
yggdrasil/dataclasses/expiring.py
Thread-safe expiring cache primitives.
Classes¶
Expiring[T]
Abstract base: subclasses implement _refresh() to reload a single value
on demand.
ExpiringDict[K, V]
Thread-safe dict where every key has an individual TTL. No subclassing
required — pass a refresher callable if per-key auto-refresh is wanted.
Pass on_evict to receive notifications when an entry leaves the cache,
so values that own external resources (file handles, spilled IO temp
files, GPU buffers, …) can release them deterministically.
Change log vs previous version
- Expiring: no new_instance callable field; subclasses implement
_refresh().
- ExpiringDict: new; built on the same time-utils / lock discipline as
Expiring.
- ExpiringDict: new on_evict callback fires for every removal path
(explicit delete, TTL expiry sweep, capacity eviction, refresher replace,
pop, clear, etc). Callback runs OUTSIDE the lock so it can do real work
without serializing other dict ops; exceptions are caught and discarded
so a bad callback can't poison the cache.
RefreshResult
dataclass
¶
RefreshResult(
value: Optional[T],
created_at_ns: int | None = None,
ttl_ns: int | None = None,
expires_at_ns: int | None = None,
)
Bases: Generic[T]
Output of _refresh().
Provide either:
- ttl_ns (duration) OR
- expires_at_ns (absolute time)
If both are None → non-expiring.
make
classmethod
¶
make(
value: Optional[T] = None,
*,
created_at_ns: Optional[_IntLike] = None,
ttl_ns: Optional[Union[_IntLike, timedelta]] = None,
expires_at_ns: Optional[_IntLike] = None
) -> "RefreshResult[T]"
Convenience constructor with defaults + light casting.
Expiring
dataclass
¶
Expiring(
_value: Optional[T] = None,
_created_at_ns: int = now_utc_ns(),
_expires_at_ns: Optional[int] = None,
_ttl_ns: Optional[int] = None,
)
Bases: Generic[T], ABC
Abstract thread-safe expiring cache holder.
Subclasses implement _refresh() which is called when .value is
accessed and the cache is expired.
ExpiringDict ¶
ExpiringDict(
default_ttl: Optional[Union[float, int, timedelta]] = 300.0,
*,
max_size: int | None = None,
refresher: Optional[Callable[[K], RefreshResult[V]]] = None,
on_evict: Optional[Callable[[K, V], None]] = None
)
Bases: Generic[K, V]
Thread-safe dictionary where every key carries an individual TTL.
Built on the same nanosecond time-utils as Expiring but
fully lockless — every mutation rides the CPython GIL atomicity
of dict.__setitem__ / dict.pop / list(dict.items())
with permissive race semantics, so the cache can never deadlock.
No subclassing required.
Parameters¶
default_ttl :
Default TTL as seconds (float), nanoseconds (int),
timedelta, or None (keys never expire unless given a per-key
TTL).
max_size :
Evict the soonest-to-expire key when capacity is reached.
refresher :
Optional Callable[[key], RefreshResult[V]]. When supplied,
an expired get will call refresher(key) and atomically replace the
entry rather than returning the default/raising.
on_evict :
Optional Callable[[key, value], None] invoked after every
removal — TTL-driven sweeps, capacity evictions, explicit deletes,
pop, clear, refresher replacements, __setitem__
overwrites of an existing key. The callback runs outside the
cache's lock so it can do real work (close a file handle, unlink
a temp file, decrement a refcount) without serializing other
cache ops. Exceptions raised by the callback are swallowed so a
bad callback can't poison the cache; if the callback's failure
matters to the caller, they should log it themselves.
Serialization¶
__getstate__ / __setstate__ are implemented: only live
(non-expired) entries are persisted. The on_evict and
refresher callbacks are NOT persisted — they're typically
closures over runtime state. Compatible with pickle,
copy.deepcopy, and joblib.
set ¶
Insert or overwrite key.
ttl accepts seconds (float), nanoseconds (int),
timedelta, or None (no expiry). Omitting ttl uses the
instance default. Schedules a background purge every 15 minutes.
Fires on_evict for any entry that was capacity-evicted to
make room or whose key got overwritten.
Lock-free. dict.__setitem__ / dict.pop /
dict.get are GIL-atomic; the capacity check and
overwrite-notify bookkeeping run under a permissive race
regime (under heavy contention the cache may briefly hold
max_size + 1 entries or notify a value that another
thread already replaced — both harmless).
get ¶
Return value for key, or default if missing / expired.
set_many ¶
Insert multiple key-value pairs sharing a TTL.
Lock-free; each insert lands via GIL-atomic
dict.__setitem__. The batch is no longer atomic
end-to-end — a concurrent reader may observe a partially
applied batch — but each individual entry's visibility is
atomic, which is all a cache needs.
Fires on_evict (after the writes) for any entries that
got capacity-evicted or overwritten.
update ¶
update(
other: Union[Dict[K, V], "ExpiringDict[K, V]", None] = None,
ttl: Any = ...,
**kwargs: V
) -> None
Update the dict from a mapping and/or keyword arguments, mirroring
the stdlib dict.update signature.
Parameters¶
other :
A plain dict, another ExpiringDict, or any object with an
.items() method. When the source is an ExpiringDict and no
explicit ttl is given, each key's remaining TTL is preserved
so expiry semantics survive a copy. Already-expired source keys
are silently skipped.
ttl :
Override TTL for every key written. Accepts seconds (float),
nanoseconds (int), timedelta, or None (no expiry).
When omitted, the instance default is used for plain-dict sources,
or the source key's remaining TTL is used for ExpiringDict
sources.
kwargs :
Additional key-value pairs merged after *other, using *ttl /
the instance default.
get_many ¶
Return {key: value} for all live keys in keys.
Pure read — no eviction side-effect. Lock-free; relies on
the GIL atomicity of dict.get. Expired entries are skipped
but left in place for the background purge / next get to
evict (the previous pop-on-expire path raced with concurrent
refreshers).
delete_many ¶
Delete keys; returns count of keys actually removed.
ttl ¶
Remaining TTL in seconds for key, or None if gone/expired.
Pure inspection — no eviction side-effect. (The previous
evict-on-expired variant raced with concurrent refresh and
could drop a freshly-set value; lazy eviction happens in
get and the background sweep.)
snapshot ¶
Shallow copy of live entries as {key: (value, expires_at_ns)}.
Useful for debugging or external persistence.
clear ¶
Drop every entry. Fires on_evict for each removed entry.
Lock-free. When on_evict is wired up we snapshot first,
then call dict.clear() — entries inserted between the
snapshot and the clear get cleared too but won't trigger the
callback (permissive). When no callback is set, the fast
path is a single GIL-atomic dict.clear() call.
purge_expired ¶
Explicitly evict all expired keys; returns count removed.
Fires on_evict for every entry that expired.
refresh_key ¶
Reset the TTL of an existing, live key.
Returns True if key existed and was refreshed, False otherwise.
Fires on_evict ONLY when the call discovers a silently
expired entry and drops it; never when the key is genuinely
refreshed.
Lock-free pop+reinsert. Permissive race: a concurrent writer that lands between our pop and our reinsert will be overwritten by ours (we restore the previous value with a new TTL). Acceptable for a TTL-bump operation.
get_or_set ¶
Return the live value for key; if missing/expired, store default (or the result of calling it) and return that.
Fires on_evict if a previously-stored expired entry got
replaced — the old value is leaving the cache.
Note: the default callable is invoked OUTSIDE the internal
lock. This rules out a class of deadlocks where default()
takes another lock (or re-enters this cache from a different
thread). The tradeoff is that under contention two threads
may both invoke default() — the first writer wins, the
loser silently discards its own computation.
apply_refresh_result ¶
Store the outcome of an external RefreshResult for key.
Mirrors the contract from Expiring._apply_refresh_result.
datetime_to_epoch_ns ¶
datetime → epoch ns (microsecond precision ⇒ trailing *000).
timedelta_to_ns ¶
timedelta → ns (microsecond precision ⇒ trailing *000).