feat: implement caching for duty-related data and enhance performance
- Added a TTLCache class for in-memory caching of duty-related data, improving performance by reducing database queries. - Integrated caching into the group duty pin functionality, allowing for efficient retrieval of message text and next shift end times. - Introduced new methods to invalidate caches when relevant data changes, ensuring data consistency. - Created a new Alembic migration to add indexes on the duties table for improved query performance. - Updated tests to cover the new caching behavior and ensure proper functionality.
This commit is contained in:
@@ -19,7 +19,7 @@ from duty_teller.db.repository import (
|
||||
ROLE_USER,
|
||||
ROLE_ADMIN,
|
||||
)
|
||||
from duty_teller.handlers.common import is_admin_async
|
||||
from duty_teller.handlers.common import invalidate_is_admin_cache, is_admin_async
|
||||
from duty_teller.i18n import get_lang, t
|
||||
from duty_teller.utils.user import build_full_name
|
||||
|
||||
@@ -230,6 +230,7 @@ async def set_role(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
ok = await asyncio.get_running_loop().run_in_executor(None, do_set_role)
|
||||
if ok:
|
||||
invalidate_is_admin_cache(target_user.telegram_user_id)
|
||||
await update.message.reply_text(
|
||||
t(lang, "set_role.done", name=target_user.full_name, role=role_name)
|
||||
)
|
||||
|
||||
@@ -3,22 +3,35 @@
|
||||
import asyncio
|
||||
|
||||
import duty_teller.config as config
|
||||
from duty_teller.cache import is_admin_cache
|
||||
from duty_teller.db.repository import is_admin_for_telegram_user
|
||||
from duty_teller.db.session import session_scope
|
||||
|
||||
|
||||
async def is_admin_async(telegram_user_id: int) -> bool:
|
||||
"""Check if Telegram user is admin (username or phone). Runs DB check in executor.
|
||||
"""Check if Telegram user is admin. Cached 60s. Invalidated on set_user_role.
|
||||
|
||||
Args:
|
||||
telegram_user_id: Telegram user id.
|
||||
|
||||
Returns:
|
||||
True if user is in ADMIN_USERNAMES or their stored phone is in ADMIN_PHONES.
|
||||
True if user is admin (DB role or env fallback).
|
||||
"""
|
||||
cache_key = ("is_admin", telegram_user_id)
|
||||
value, found = is_admin_cache.get(cache_key)
|
||||
if found:
|
||||
return value
|
||||
|
||||
def _check() -> bool:
|
||||
with session_scope(config.DATABASE_URL) as session:
|
||||
return is_admin_for_telegram_user(session, telegram_user_id)
|
||||
|
||||
return await asyncio.get_running_loop().run_in_executor(None, _check)
|
||||
result = await asyncio.get_running_loop().run_in_executor(None, _check)
|
||||
is_admin_cache.set(cache_key, result)
|
||||
return result
|
||||
|
||||
|
||||
def invalidate_is_admin_cache(telegram_user_id: int | None) -> None:
|
||||
"""Invalidate is_admin cache for user. Call after set_user_role."""
|
||||
if telegram_user_id is not None:
|
||||
is_admin_cache.invalidate(("is_admin", telegram_user_id))
|
||||
|
||||
@@ -15,10 +15,11 @@ from duty_teller.db.session import session_scope
|
||||
from duty_teller.i18n import get_lang, t
|
||||
from duty_teller.services.group_duty_pin_service import (
|
||||
get_duty_message_text,
|
||||
get_message_id,
|
||||
get_next_shift_end_utc,
|
||||
get_pin_refresh_data,
|
||||
save_pin,
|
||||
delete_pin,
|
||||
get_message_id,
|
||||
get_all_pin_chat_ids,
|
||||
)
|
||||
|
||||
@@ -28,6 +29,14 @@ JOB_NAME_PREFIX = "duty_pin_"
|
||||
RETRY_WHEN_NO_DUTY_MINUTES = 15
|
||||
|
||||
|
||||
def _sync_get_pin_refresh_data(
|
||||
chat_id: int, lang: str = "en"
|
||||
) -> tuple[int | None, str, datetime | None]:
|
||||
"""Get message_id, duty text, next_shift_end in one DB session."""
|
||||
with session_scope(config.DATABASE_URL) as session:
|
||||
return get_pin_refresh_data(session, chat_id, config.DUTY_DISPLAY_TZ, lang)
|
||||
|
||||
|
||||
def _get_duty_message_text_sync(lang: str = "en") -> str:
|
||||
with session_scope(config.DATABASE_URL) as session:
|
||||
return get_duty_message_text(session, config.DUTY_DISPLAY_TZ, lang)
|
||||
@@ -96,26 +105,27 @@ async def _refresh_pin_for_chat(
|
||||
) -> Literal["updated", "no_message", "failed"]:
|
||||
"""Refresh pinned duty message: send new message, unpin old, pin new, save new message_id.
|
||||
|
||||
Uses single DB session for message_id, text, next_shift_end (consolidated).
|
||||
|
||||
Returns:
|
||||
"updated" if the message was sent, pinned and saved successfully;
|
||||
"no_message" if there is no pin record for this chat;
|
||||
"failed" if send_message or permissions failed.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
message_id = await loop.run_in_executor(None, _sync_get_message_id, chat_id)
|
||||
message_id, text, next_end = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _sync_get_pin_refresh_data(chat_id, config.DEFAULT_LANGUAGE),
|
||||
)
|
||||
if message_id is None:
|
||||
logger.info("No pin record for chat_id=%s, skipping update", chat_id)
|
||||
return "no_message"
|
||||
text = await loop.run_in_executor(
|
||||
None, lambda: _get_duty_message_text_sync(config.DEFAULT_LANGUAGE)
|
||||
)
|
||||
try:
|
||||
msg = await context.bot.send_message(chat_id=chat_id, text=text)
|
||||
except (BadRequest, Forbidden) as e:
|
||||
logger.warning(
|
||||
"Failed to send duty message for pin refresh chat_id=%s: %s", chat_id, e
|
||||
)
|
||||
next_end = await loop.run_in_executor(None, _get_next_shift_end_sync)
|
||||
await _schedule_next_update(context.application, chat_id, next_end)
|
||||
return "failed"
|
||||
try:
|
||||
@@ -127,11 +137,9 @@ async def _refresh_pin_for_chat(
|
||||
)
|
||||
except (BadRequest, Forbidden) as e:
|
||||
logger.warning("Unpin or pin after refresh failed chat_id=%s: %s", chat_id, e)
|
||||
next_end = await loop.run_in_executor(None, _get_next_shift_end_sync)
|
||||
await _schedule_next_update(context.application, chat_id, next_end)
|
||||
return "failed"
|
||||
await loop.run_in_executor(None, _sync_save_pin, chat_id, msg.message_id)
|
||||
next_end = await loop.run_in_executor(None, _get_next_shift_end_sync)
|
||||
await _schedule_next_update(context.application, chat_id, next_end)
|
||||
return "updated"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user