fleche.storage.thread_safe
Thread-safety mixins for storage classes.
These mixins subclass KeyManagement and override
_operation_context to inject a threading lock around every storage
operation. Because every concrete storage class (ValueMemory, Sql, …)
ultimately inherits from KeyManagement, these mixins compose with any
backend via Python’s MRO:
@dataclass(frozen=True)
class ThreadSafeValueMemory(SerializingMixin, ValueMemory): ...
Choose the mixin based on the access pattern you need:
SerializingMixin— single globalRLock. Every operation waits for the same lock, so the storage is touched by at most one thread at a time. Use when the backing store is not thread-safe and per-key parallelism is not needed.PerKeyLockMixin— a striped lock table, oneRLockper key. Operations on different keys proceed in parallel; operations on the same key are serialized. Use when contention is dominated by hot keys and the backing store supports concurrent access on disjoint keys.
Both mixins use reentrant locks so nested acquisitions (e.g. expand being
called inside load) do not deadlock.
Classes
Mixin that serializes all storage operations behind a single reentrant lock. |
|
Mixin that locks per-key so concurrent ops on different keys proceed in parallel. |
Module Contents
- class fleche.storage.thread_safe.SerializingMixin[source]
Bases:
fleche.storage.base.KeyManagementMixin that serializes all storage operations behind a single reentrant lock.
Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class SerializingValueMemory(SerializingMixin, ValueMemory): ...
- _operation_context(key)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key): with self._lock: # this mixin's resource with super()._operation_context(key): yield
- class fleche.storage.thread_safe.PerKeyLockMixin[source]
Bases:
fleche.storage.base.KeyManagementMixin that locks per-key so concurrent ops on different keys proceed in parallel.
A lightweight
threading.Lockguards the lock-table itself; once the per-keyRLockis obtained the table lock is released, so two threads operating on different keys never block each other. Operations on the same key are serialized by the per-key lock, which is reentrant to allow nested calls (e.g.expandinsideload).Place before the concrete storage class in the MRO:
@dataclass(frozen=True) class PerKeyValueMemory(PerKeyLockMixin, ValueMemory): ...
- _key_locks: weakref.WeakValueDictionary[fleche.digest.Digest | str, threading.RLock][source]
- _get_key_lock(key: fleche.digest.Digest | str) threading.RLock[source]
- _operation_context(key)[source]
Context manager entered around every operation on
key.The base implementation is a no-op. Override in a mixin to inject any resource scoped to the operation — a threading lock, a SQLAlchemy session, an open file handle, a decompression stream, etc.
Receiving
keylets implementations choose between a single global resource (ignore the key) or per-key resources (e.g. a striped lock table or a key-specific file handle).Composing multiple mixins: use
super()to chain so that every mixin in the MRO gets to wrap the operation:@contextlib.contextmanager def _operation_context(self, key): with self._lock: # this mixin's resource with super()._operation_context(key): yield