Extensions

Cache

Our event query happily counts models on every call. That is fine until the table grows or the query gets expensive. The Cache battery gives you one small key-value API that swaps its backend behind the scenes, so you can keep hot values around without committing to a specific store.

Two backends, one API

The extension ships two implementations. Redis is the production backend and the default. Legacy is a simple in-process dictionary that expires its own entries through an expiry heap — great for development and single-process setups, and it needs nothing but itself. You pick one with CACHE_TYPE:

app_configs/app.ini ini
[general]
SECRET_KEY = supersecret

[data]
REDIS_URI = redis://localhost:6379

[extensions]
EXT_CACHE = 1

[cache]
CACHE_TYPE = legacy
 

If you do not set CACHE_TYPE at all, the framework default resolves to redis and connects to REDIS_URI database 2. The legacy backend shown above keeps everything in one process, so it is the wrong choice the moment you run more than one worker — two processes, two caches, two answers.

The config values

  • CACHE_TYPElegacy or redis.
  • CACHE_DEFAULT_TIMEOUT — TTL in seconds when you don't pass one. Default 300.
  • CACHE_REDIS_URI — the redis url for the redis backend.

Using the cache

Like every battery, the instance lives in the shared registry, and every method has an async twin. Let's make our model count from the previous chapter cheap to ask for:

fluid/events/models.py python
from webfluid.core.ext import db, cache, events
from sqlalchemy import select, func

from fluid.models import MyModel


def register():
    events.create_signal("model:created", internal=False)

    @events.query("model:count")
    async def count_models(_):
        cached = await cache.aget("model:count")
        if cached is not None:
            return int(cached)

        async with db.async_executor(model=MyModel) as e:
            result = await e.exec(select(func.count(MyModel.id)), scalars=False)
            total = result.scalar()

        # Keep it warm for a minute.
        await cache.aset("model:count", total, timeout=60)
        return total

    # Whenever a model is created, drop the stale count:
    @events.event("model:created", internal=False)
    async def bust_count(_):
        await cache.adelete("model:count")

The synchronous side mirrors this one to one with cache.set, cache.get and cache.delete, plus clear / aclear to wipe everything. Values are stored as-is by the legacy backend and as strings by redis, so decode on the way out when you need a specific type.

 

Two edges worth knowing before you reach for either. clear() is a flushdb: it does not clear your keys, it clears the database — and in an app that uses JWT that includes the signing keys, which invalidates every token in circulation. And a timeout of 0 is not “no expiry”; it is falsy, so it silently becomes CACHE_DEFAULT_TIMEOUT. There is no way to write an entry that never expires.

Continue reading

From here you can continue straight with JWTManager.