async-lru

async-lru

async-lru — это библиотека для кэширования результатов асинхронных функций. По смыслу она похожа на стандартный functools.lru_cache, но рассчитана на asyncio: она умеет кешировать awaitable-результаты и не допускает гонок при одновременных вызовах одного и того же ключа.

Главная особенность: если несколько задач одновременно обращаются к одной и той же функции с одинаковыми аргументами, то реально будет выполнен только один вызов. Все остальные await получат тот же результат после завершения вычисления.

Это особенно полезно для:

  • HTTP-запросов к внешним API;
  • чтения данных из Redis/DB/Postgres;
  • повторных вычислений на одних и тех же аргументах;
  • уменьшения thundering herd при массовом параллельном доступе.

Базовый пример

import asyncio
from async_lru import alru_cache


@alru_cache(maxsize=128)
async def fetch_user(user_id: int):
    await asyncio.sleep(0.5)
    return {"id": user_id, "name": f"user-{user_id}"}


async def main():
    first = await fetch_user(42)
    second = await fetch_user(42)

    print(first)
    print(second)
    print(fetch_user.cache_info())
    await fetch_user.cache_close()


asyncio.run(main())

Что происходит здесь:

  • @alru_cache(maxsize=128) добавляет LRU-кэш для async-функции;
  • одинаковые аргументы возвращают данные из кэша;
  • разовое вычисление результата делится между всеми конкурентными вызовами;
  • cache_info() показывает статистику: hits, misses, currsize, maxsize.

Почему это важно

Обычный кэш для async-функций часто пишут руками через словарь и asyncio.Lock, но это быстро превращается в ошибочный и сложный код. async-lru решает типичную проблему: “несколько задач одновременно могут начать одно и то же expensive-вычисление”.

Без защиты такой сценарий приводит к повторным запросам к API, лишним вычислениям и лавине параллельных вызовов. async-lru как раз устраняет этот эффект, сохраняя семантику LRU-кэша.

TTL и expiration

Библиотека поддерживает TTL (time-to-live) для кэшированных значений. Это удобно, если данные быстро устаревают и хочется периодически обновлять их без полного сброса кэша.

from async_lru import alru_cache


@alru_cache(ttl=5)
async def get_config(name: str):
    return name.upper()

Для более мягкого обновления можно использовать jitter:

@alru_cache(ttl=3600, jitter=1800)
async def fetch_data(key):
    ...

Тогда TTL каждого элемента будет случайно “размазаны” по диапазону, что помогает избегать массовой синхронной просушки кэша.

Инвалидация и проверка содержимого

Библиотека умеет явно очищать элементы кэша.

@alru_cache(ttl=5)
async def func(arg1, arg2):
    return arg1 + arg2

func.cache_invalidate(1, arg2=2)

Также есть проверка, есть ли конкретный ключ в кэше:

await func(1, arg2=2)
print(func.cache_contains(1, arg2=2))  # True

И методы для очистки всего кэша:

func.cache_clear()

Кастомные ключи

По умолчанию ключ строится из аргументов функции так же, как functools.lru_cache. Но иногда часть аргументов не должна участвовать в ключе. Для этого есть параметр key.

from async_lru import alru_cache


@alru_cache(key=lambda db, query: query)
async def query_db(db, query):
    return await db.execute(query)

Это удобно, когда один и тот же запрос должен кэшироваться независимо от того, какой именно объект соединения использовался.

Ограничения и важные детали

У async-lru есть важное ограничение: кэш привязан к конкретному event loop. Если один и тот же декорированный объект начать использовать в другом event loop, библиотека выбросит RuntimeError.

Это нормально для типичных asyncio-приложений, где один loop на приложение, но важно помнить:

  • если сервис работает на нескольких loop, нужно создавать отдельные кэш-экземпляры;
  • важно не смешивать одни и те же cached-функции между потоками или loop.

Также библиотека строит ключ только из явных аргументов. Если результат зависит от contextvars, request-scoped state, текущего пользователя, tenant id или глобальных переменных, то это нужно явно включать в аргументы функции или использовать отдельный кэш для каждого security-domain.

Когда использовать async-lru

Используйте async-lru, когда:

  • у вас async-функция делает expensive I/O или вычисления;
  • один и тот же запрос может повторяться много раз;
  • важно не только кэшировать результаты, но и дедуплицировать concurrent calls;
  • нужен простой LRU-подход без написания кастомного кэша.

Типичные сценарии:

  • кеширование response body из API;
  • мемоизация запросов к базе и сервисам;
  • уменьшение повторной нагрузки при burst-traffic.

Сравнение с functools.lru_cache

functools.lru_cache хорошо работает для sync-функций, но не подходит для async-кода. async-lru же:

  • работает с async def;
  • обеспечивают совместный результат для concurrent requests;
  • поддерживает TTL и явную инвалидацию;
  • сохраняет идеи LRU и cache_info в привычной форме.

Дополнительно

Источник

AI-warning

✨ Данная статья сгенерирована с помощью ChatGPT 5.0 mini. Текст проверен человеком.