yggdrasil.dataclasses.singleton¶
singleton ¶
Hash-keyed singleton base class — process-wide instance cache.
Pairs __new__ with an :class:ExpiringDict so two constructor
calls that produce the same hashable key collapse to one instance.
The singleton key is whatever :meth:Singleton._singleton_key
projects out of the constructor arguments — by default (cls,
args, sorted kwargs items), which works for any constructor
whose arguments are hashable.
Subclasses with non-trivial constructors override
:meth:_singleton_key to:
- drop fields that don't participate in instance identity;
- normalize unhashable inputs (
dict→frozensetof items,list→tuple); - canonicalize semantically equivalent forms (e.g. trailing slash on a URL) so different spellings of the same identity collapse.
__init__ must be idempotent: Python re-invokes it on every
constructor call, even when __new__ returns the cached
instance, so subclasses guard with if getattr(self,
"_initialized", False): return to avoid clobbering live state
on the second pass.
Pickling is wired through :meth:__getstate__ / :meth:__setstate__:
attribute names listed in _TRANSIENT_STATE_ATTRS are excluded
from the payload (live SDK clients, sockets, lazy service
caches), and __setstate__ short-circuits when the receiver is
already initialized so unpickling collapses to the live
in-process singleton.
Singleton ¶
Base class that caches one instance per hashable constructor key.
The cache is shared across every subclass (the default
_singleton_key includes cls so different subclasses can
coexist in one dict). A subclass that wants a private cache
re-declares its own _INSTANCES ClassVar.
No mutex anywhere — :meth:__new__, :meth:to_singleton, and
:meth:invalidate_singleton all ride :class:ExpiringDict's
lockless GIL-atomic primitives (get_or_set for atomic
check-and-insert, pop for atomic identity-guarded remove).
Cannot deadlock by construction.
to_singleton ¶
Promote this instance into the per-class _INSTANCES cache.
Hot listing paths (iterdir / _ls / glob) build
children with singleton_ttl=False so the bounded cache
doesn't fill up with thousands of short-lived entries. When a
caller decides one of those children is worth keeping around
(handing it to a long-running worker, returning it from an
API), :meth:to_singleton registers self into the cache
so the next constructor call with the same key collapses to
the same instance.
ttl defaults to the subclass's _SINGLETON_TTL
(... = no caching, None = process lifetime, or a
seconds count). When a different instance is already cached
under this key, that pre-existing one wins and is returned
unchanged — the cache is the source of truth.
invalidate_singleton ¶
Pop self from the per-class _INSTANCES cache.
Mutating ops on a Singleton-cached object (writes, deletes,
schema invalidations on a Databricks table, put_object on
an :class:S3Path) want to make sure the next caller asking
for the same key gets a fresh build rather than collapsing
onto this stale handle — that's what remove_global=True
(the default) does. The pop is :meth:identity-guarded:
only an entry that still points at self is removed, so
a concurrent re-construction that already raced past this
thread is left alone.
remove_global=False is a no-op. The keyword exists so
subclass invalidators (invalidate_singleton,
_invalidate_entity_tag_cache, …) can offer the same
switch without branching at the call site.