Skip to content

Background Cleanup

psycache ignores expired keys when reading, but their rows stick around until something deletes them. You can call PostgresCache.cleanup_expired() yourself, or let psycache do it for you in the background.

A cleanup thread

For synchronous pools, PostgresCache.start_cleanup_thread() starts a daemon thread that periodically deletes expired entries. Use it as a context manager to stop the thread automatically:

from sqlalchemy import create_engine

from psycache import PostgresCache
from psycache.sqlalchemy import SQLAlchemyCachePool


engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
cache = PostgresCache(SQLAlchemyCachePool(engine))


with cache.start_cleanup_thread(interval=60):
    ...  # your application runs here

Or manage its lifecycle manually through the returned CleanupService:

svc = cache.start_cleanup_thread(interval=60)
try:
    ...  # your application runs here
finally:
    svc.stop()

engine.dispose()

… or a cleanup task

For async pools, use AsyncPostgresCache.start_cleanup_task() inside a running event loop. It starts an asyncio.Task that periodically deletes expired entries and can be used as an async context manager.

Otherwise, it mirrors the behavior of PostgresCache.start_cleanup_thread():

import asyncio

from sqlalchemy.ext.asyncio import create_async_engine

from psycache import AsyncPostgresCache
from psycache.sqlalchemy import AsyncSQLAlchemyCachePool


aengine = create_async_engine(
    "postgresql+psycopg://psycache@127.0.0.1/psycache"
)
acache = AsyncPostgresCache(AsyncSQLAlchemyCachePool(aengine))


async def main():
    async with acache.start_cleanup_task(interval=60):
        ...  # your application runs here

    svc = acache.start_cleanup_task(interval=60)
    try:
        ...  # your application runs here
    finally:
        await svc.stop()


asyncio.run(main())

… or an elected pgbg service

Both helpers above run the cleanup in every process that starts them. That’s harmless, because deleting expired rows is idempotent. But if you run many instances of your application, it’s wasteful, because all of them delete the same rows at the same interval, and most of them find nothing to do.

pgbg solves this with PostgreSQL-based leader election: many processes can start the same service, but only one of them runs it at a time. On top of that, pgbg supervises the thread and restarts it after a crash.

Wrap PostgresCache.cleanup_expired() in a work unit and start it as an elected service (example requires pgbg[sqlalchemy], but works with raw Psycopg, too):

import pgbg

from pgbg.sqlalchemy import start_elected_service
from sqlalchemy import create_engine

from psycache import PostgresCache
from psycache.sqlalchemy import SQLAlchemyCachePool


engine = create_engine("postgresql+psycopg://psycache@127.0.0.1/psycache")
cache = PostgresCache(SQLAlchemyCachePool(engine))

# *pgbg* requires a lease database; you only do this once.
with engine.connect() as conn:
    pgbg.init_db(conn.connection.driver_connection)


def cleanup_cache() -> bool:
    cache.cleanup_expired()

    return False  # done until the next interval


with start_elected_service(
    pgbg.as_work_factory(cleanup_cache),
    engine,
    name="psycache-cleanup",
    worker_id="worker-01.example.internal",  # unique per process
    wakeup=pgbg.IntervalOnlyWakeup(),
    interval=60,
):
    ...  # your application runs here

engine.dispose()

pgbg is thread-based and runs plain, non-async functions. So pair it with the synchronous PostgresCache, even if the rest of your application is async.