feat: implement caching for duty-related data and enhance performance
All checks were successful
CI / lint-and-test (push) Successful in 24s
Docker Build and Release / build-and-push (push) Successful in 49s
Docker Build and Release / release (push) Successful in 8s

- 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:
2026-02-25 13:25:34 +03:00
parent 5334a4aeac
commit 0e8d1453e2
14 changed files with 413 additions and 113 deletions

View File

@@ -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"