Skip to content

Caches

Choose by who needs the value.

In the process: LocalCache

Use this for what no other process needs. Its methods are synchronous, and values stay Python objects:

from myxo.cache import LocalCache

thumbnails = LocalCache(ttl=60.0, size=10_000)

thumbnails.set("item:1", data)
data = thumbnails.get("item:1")   # None if absent or expired
thumbnails.delete("item:1")

Past size, the oldest values go.

Shared by the installation: Cache

Use this for what processes share, such as permissions that a revocation must reach in every replica. Declare cache = Cache(), and give get the type to decode:

from myxo.cache import Cache


class Empty(msgspec.Struct, frozen=True):
    pass


class Stats(msgspec.Struct, frozen=True):
    seen: int


class Notes(Service):
    name = "notes"
    cache = Cache()

    async def stats(self, _: Empty) -> Stats:
        return await self.cache.get("seen", Stats) or Stats(0)

    async def record(self, seen: int) -> None:
        await self.cache.set("seen", Stats(seen), ttl=300.0)

Values are anything msgspec encodes, and get returns None for a key that is absent or expired. The cache lives in the layout's database, so the layout needs a database, and myxo migrate creates the table. Every service and process of the installation reads the same keys, so prefix yours with the service's name, as in notes:seen.

What a shared cache promises

On PostgreSQL its table is unlogged, and writes do not wait for the drive. A crash of the database may empty it, which only costs misses. So:

  • Keep nothing in it that you can't compute again.
  • set is fast, and may be lost in a crash.
  • delete is durable, so that a revocation stays revoked.

Each process removes expired entries now and then, every 60 s.