Skip to content

InMemoryCacheAdapter — In-Memory Cache Backend

A CacheProviderInterface implementation for tests and single-process usage, with no external Redis dependency. Locks are process-local.

InMemoryCacheAdapter

InMemoryCacheAdapter(
    key_prefix: str = "test:", cache_name: str = "memory"
)

Bases: CacheProviderInterface

Thread-safe in-memory cache implementation.

This adapter provides a complete cache implementation using Python dictionaries with thread-safe operations.

Features
  • Full CacheProviderInterface compliance
  • Thread-safe operations
  • TTL support with lazy expiration
  • Mock distributed locks (single-process only)
Example

cache = InMemoryCacheAdapter() cache.set("key", {"data": "value"}, ttl=timedelta(minutes=5)) data = cache.get("key") with cache.get_lock("my_lock") as lock: ... # Critical section (only within single process!) ... pass

Warning

This is for TESTING ONLY. Data is not persisted and locks only work within a single process.

Initialize in-memory cache.

Parameters:

Name Type Description Default
key_prefix str

Prefix for all cache keys

'test:'
cache_name str

Name for metrics identification

'memory'

provider_name property

provider_name: str

Return 'memory' as the provider identifier.

cleanup_expired

cleanup_expired() -> int

Lock-acquiring entry point for periodic expiration sweeps.

Wraps the unlocked _cleanup_expired() with self._lock so external callers (CleanupService) can safely invoke it. self._lock is a non-reentrant threading.Lock; reusing the private method from a non-locked context would deadlock.

Returns:

Type Description
int

Number of expired entries removed.

clear_all_instances classmethod

clear_all_instances() -> None

Clear the class-level weak registry (for test fixtures).

get

get(key: str) -> Any | None

Get value by key.

set

set(
    key: str, value: Any, ttl: timedelta | None = None
) -> bool

Set value with optional TTL.

delete

delete(key: str) -> bool

Delete key from cache.

exists

exists(key: str) -> bool

Check if key exists in cache.

incr

incr(key: str, amount: int = 1) -> int

Atomically increment a counter.

decr

decr(key: str, amount: int = 1) -> int

Atomically decrement a counter.

expire

expire(key: str, ttl: timedelta) -> bool

Set expiration on existing key.

ttl

ttl(key: str) -> int | None

Get remaining TTL in seconds.

setnx

setnx(
    key: str, value: Any, ttl: timedelta | None = None
) -> bool

Set value only if key does not exist.

cas_dict_field

cas_dict_field(
    key: str,
    field: str,
    expected: Any,
    new_value: dict[str, Any],
    ttl: timedelta | None = None,
) -> bool

Atomic single-field CAS on a dict-valued record (lock-wrapped).

cas_takeover

cas_takeover(
    key: str,
    new_record: dict[str, Any],
    *,
    stale_before: float,
    ttl: timedelta | None = None
) -> bool

Atomic failed / stale-executing takeover (lock-wrapped).

Replaces the record iff it is a dict whose status == "failed" OR (status == "executing" AND started_at < stale_before). No I/O to fail, so — like setnx / cas_dict_field on this adapter — it never raises AdapterConnectionError.

get_lock

get_lock(
    name: str,
    timeout: timedelta = timedelta(seconds=10),
    blocking_timeout: float | None = None,
) -> DistributedLock

Get a distributed lock instance.

Resolves the registry key once via self._make_key(name) so two adapter instances with different key_prefix map the same user-facing name to distinct entries in the class-level InMemoryLock._locks registry.

mget

mget(keys: list[str]) -> dict[str, Any]

Get multiple values at once.

mset

mset(
    mapping: dict[str, Any], ttl: timedelta | None = None
) -> bool

Set multiple values at once.

mdelete

mdelete(keys: list[str]) -> int

Delete multiple keys at once.

push_limit

push_limit(
    key: str,
    value: Any,
    max_len: int,
    ttl: timedelta | None = None,
) -> int

Append value to a list and trim to max_len under thread lock.

list_range

list_range(key: str, start: int, end: int) -> list[Any]

Return elements from start to end (inclusive).

health_check

health_check() -> bool

Check if cache is healthy.

set_health_status

set_health_status(healthy: bool) -> None

Set health status for testing.

flush_all

flush_all() -> bool

Clear all keys.

keys

keys(pattern: str = '*') -> list[str]

Find keys matching a pattern.

scan

scan(
    pattern: str = "*", count: int = 100
) -> tuple[int, list[str]]

Incrementally iterate keys matching a pattern.

get_store_size

get_store_size() -> int

Get number of entries in store (for testing).

clear_all

clear_all() -> None

Clear entire store including all prefixes (for testing cleanup).

Async in-memory adapter

An AsyncCacheProviderInterface implementation backing the async idempotency fallback path and serving as the async dedup test double (no Redis, no fakeredis).

AsyncInMemoryCacheAdapter

AsyncInMemoryCacheAdapter(key_prefix: str = 'test:')

Bases: AsyncCacheProviderInterface

Thread-unsafe-but-loop-atomic in-memory async cache for dedup ops.

Example

cache = AsyncInMemoryCacheAdapter() await cache.asetnx("k", {"status": "executing"}, ttl=timedelta(minutes=5)) True await cache.aget("k")

Initialize the async in-memory cache.

Parameters:

Name Type Description Default
key_prefix str

Prefix applied to every stored key so distinct layers / instances cannot collide on the same logical key.

'test:'

provider_name property

provider_name: str

Return 'memory' as the provider identifier.

asetnx async

asetnx(
    key: str, value: Any, ttl: timedelta | None = None
) -> bool

Set only if absent — atomic acquire (no await between check and set).

acas_dict_field async

acas_dict_field(
    key: str,
    field: str,
    expected: Any,
    new_value: dict[str, Any],
    ttl: timedelta | None = None,
) -> bool

Atomic single-field CAS on a dict record (no await between read/write).

acas_takeover async

acas_takeover(
    key: str,
    new_record: dict[str, Any],
    *,
    stale_before: float,
    ttl: timedelta | None = None
) -> bool

Atomic failed / stale-executing takeover (no await between read/write).

Replaces the record iff it is a dict whose status == "failed" OR (status == "executing" AND started_at < stale_before). In-process only; no I/O to fail, so it never raises AdapterConnectionError.

clear_all

clear_all() -> None

Clear the entire store (test cleanup).

aclose async

aclose() -> None

No-op close — symmetric with :class:AsyncRedisCacheAdapter.

The in-memory adapter holds no sockets, so there is nothing to drain; the method exists so the resolver can treat either backing uniformly.