CacheStack

A CacheStack allows combining multiple caches into a single prioritized hierarchy. It is a common pattern to combine a fast, local cache (like an in-memory cache) with a larger, slower, and potentially remote cache (like a database or file system).

Behavior

  • Saving: When saving a result, it is always written to the first cache in the stack.

  • Loading: When loading a result, the stack is traversed starting from the base cache (index 0) through each fallback cache in order.

  • Automatic Hit Transfer: If a result is found in a fallback cache (index > 0), it is automatically copied down to the base cache (index 0). This ensures that frequently accessed data migrates to the fastest cache in your stack.

Example

>>> from fleche import fleche, cache
>>> from fleche.caches import Cache, CacheStack
>>> from fleche.storage import ValueMemory, CallMemory

>>> # Define two caches
>>> local_cache = Cache(ValueMemory({}), CallMemory({}))
>>> remote_cache = Cache(ValueMemory({}), CallMemory({}))

>>> @fleche
... def my_function(x):
...     return x * 2

>>> # Populate remote_cache with a result
>>> with cache(remote_cache):
...     my_function(10)

>>> # Create a stack: local_cache is the base cache (checked first), remote_cache is the fallback
>>> stack = CacheStack((local_cache, remote_cache))

>>> with cache(stack):
...     # Result is found in remote_cache and automatically copied to local_cache
...     my_function(10)

Automatic hit transfer only applies to full function calls (Call objects) and not to individual values loaded via load_value.

Layering Caches at Runtime

Rather than constructing a CacheStack by hand, you can push a new cache on top of whichever cache is currently active with push(), or its context-manager shortcut cache(new_cache, stack=True):

>>> from fleche import cache

>>> cache(remote_cache)  # sticky: remote_cache becomes the active cache

>>> with cache(local_cache, stack=True):
...     # local_cache is pushed on top: it is now stack[0] (checked and
...     # saved to first), remote_cache is the fallback.
...     ...
>>> # remote_cache is restored on exit

push always flattens rather than nests: pushing onto an existing CacheStack prepends the new cache to that stack’s members instead of wrapping the whole stack, so stack=True never runs into the nesting restriction below.

Restrictions

CacheStack cannot be nested: passing a CacheStack as a member of another CacheStack raises ValueError immediately:

inner = CacheStack((cache_a, cache_b))
outer = CacheStack((inner, cache_c))  # raises ValueError

Flatten the layers directly instead:

flat = CacheStack((cache_a, cache_b, cache_c))

Or build up the same flattened stack with chained push calls, which is handy when each cache becomes available one at a time:

flat = cache_c.push(cache_b).push(cache_a)  # CacheStack((cache_a, cache_b, cache_c))