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:
@@ -403,6 +403,9 @@ def test_calendar_ical_ignores_unknown_query_params(
|
||||
"""Unknown query params (e.g. events=all) are ignored; response is duty-only."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from duty_teller.cache import ics_calendar_cache
|
||||
|
||||
ics_calendar_cache.invalidate(("personal_ics", 1))
|
||||
mock_user = SimpleNamespace(id=1, full_name="User A")
|
||||
mock_get_user.return_value = mock_user
|
||||
duty = SimpleNamespace(
|
||||
|
||||
@@ -103,6 +103,7 @@ class TestGetDutyMessageText:
|
||||
"""Tests for get_duty_message_text."""
|
||||
|
||||
def test_no_current_duty_returns_no_duty(self, session):
|
||||
svc.duty_pin_cache.invalidate_pattern(("duty_message_text",))
|
||||
with patch(
|
||||
"duty_teller.services.group_duty_pin_service.get_current_duty",
|
||||
return_value=None,
|
||||
@@ -113,6 +114,7 @@ class TestGetDutyMessageText:
|
||||
assert result == "No duty"
|
||||
|
||||
def test_with_current_duty_returns_formatted(self, session, duty, user):
|
||||
svc.duty_pin_cache.invalidate_pattern(("duty_message_text",))
|
||||
with patch(
|
||||
"duty_teller.services.group_duty_pin_service.get_current_duty",
|
||||
return_value=(duty, user),
|
||||
@@ -130,6 +132,7 @@ class TestGetNextShiftEndUtc:
|
||||
"""Tests for get_next_shift_end_utc."""
|
||||
|
||||
def test_no_next_shift_returns_none(self, session):
|
||||
svc.duty_pin_cache.invalidate(("next_shift_end",))
|
||||
with patch(
|
||||
"duty_teller.services.group_duty_pin_service.get_next_shift_end",
|
||||
return_value=None,
|
||||
@@ -138,6 +141,7 @@ class TestGetNextShiftEndUtc:
|
||||
assert result is None
|
||||
|
||||
def test_has_next_shift_returns_naive_utc(self, session):
|
||||
svc.duty_pin_cache.invalidate(("next_shift_end",))
|
||||
naive = datetime(2025, 2, 21, 6, 0, 0)
|
||||
with patch(
|
||||
"duty_teller.services.group_duty_pin_service.get_next_shift_end",
|
||||
|
||||
@@ -143,14 +143,12 @@ async def test_update_group_pin_sends_new_unpins_pins_saves_schedules_next():
|
||||
context.application.job_queue.run_once = MagicMock()
|
||||
|
||||
with patch.object(config, "DUTY_PIN_NOTIFY", True):
|
||||
with patch.object(mod, "_sync_get_message_id", return_value=1):
|
||||
with patch.object(
|
||||
mod, "_get_duty_message_text_sync", return_value="Current duty"
|
||||
):
|
||||
with patch.object(mod, "_get_next_shift_end_sync", return_value=None):
|
||||
with patch.object(mod, "_schedule_next_update", AsyncMock()):
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
await mod.update_group_pin(context)
|
||||
with patch.object(
|
||||
mod, "_sync_get_pin_refresh_data", return_value=(1, "Current duty", None)
|
||||
):
|
||||
with patch.object(mod, "_schedule_next_update", AsyncMock()):
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
await mod.update_group_pin(context)
|
||||
context.bot.send_message.assert_called_once_with(chat_id=123, text="Current duty")
|
||||
context.bot.unpin_chat_message.assert_called_once_with(chat_id=123)
|
||||
context.bot.pin_chat_message.assert_called_once_with(
|
||||
@@ -168,7 +166,9 @@ async def test_update_group_pin_no_message_id_skips():
|
||||
context.bot = MagicMock()
|
||||
context.bot.send_message = AsyncMock()
|
||||
|
||||
with patch.object(mod, "_sync_get_message_id", return_value=None):
|
||||
with patch.object(
|
||||
mod, "_sync_get_pin_refresh_data", return_value=(None, "No duty", None)
|
||||
):
|
||||
await mod.update_group_pin(context)
|
||||
context.bot.send_message.assert_not_called()
|
||||
|
||||
@@ -185,13 +185,11 @@ async def test_update_group_pin_send_raises_no_unpin_pin_schedule_still_called()
|
||||
context.bot.pin_chat_message = AsyncMock()
|
||||
context.application = MagicMock()
|
||||
|
||||
with patch.object(mod, "_sync_get_message_id", return_value=2):
|
||||
with patch.object(mod, "_get_duty_message_text_sync", return_value="Text"):
|
||||
with patch.object(mod, "_get_next_shift_end_sync", return_value=None):
|
||||
with patch.object(
|
||||
mod, "_schedule_next_update", AsyncMock()
|
||||
) as mock_schedule:
|
||||
await mod.update_group_pin(context)
|
||||
with patch.object(
|
||||
mod, "_sync_get_pin_refresh_data", return_value=(2, "Text", None)
|
||||
):
|
||||
with patch.object(mod, "_schedule_next_update", AsyncMock()) as mock_schedule:
|
||||
await mod.update_group_pin(context)
|
||||
context.bot.unpin_chat_message.assert_not_called()
|
||||
context.bot.pin_chat_message.assert_not_called()
|
||||
mock_schedule.assert_called_once_with(context.application, 111, None)
|
||||
@@ -214,15 +212,15 @@ async def test_update_group_pin_repin_raises_still_schedules_next():
|
||||
context.application = MagicMock()
|
||||
|
||||
with patch.object(config, "DUTY_PIN_NOTIFY", True):
|
||||
with patch.object(mod, "_sync_get_message_id", return_value=3):
|
||||
with patch.object(mod, "_get_duty_message_text_sync", return_value="Text"):
|
||||
with patch.object(mod, "_get_next_shift_end_sync", return_value=None):
|
||||
with patch.object(
|
||||
mod, "_schedule_next_update", AsyncMock()
|
||||
) as mock_schedule:
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
with patch.object(mod, "logger") as mock_logger:
|
||||
await mod.update_group_pin(context)
|
||||
with patch.object(
|
||||
mod, "_sync_get_pin_refresh_data", return_value=(3, "Text", None)
|
||||
):
|
||||
with patch.object(
|
||||
mod, "_schedule_next_update", AsyncMock()
|
||||
) as mock_schedule:
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
with patch.object(mod, "logger") as mock_logger:
|
||||
await mod.update_group_pin(context)
|
||||
context.bot.send_message.assert_called_once_with(chat_id=222, text="Text")
|
||||
mock_save.assert_not_called()
|
||||
mock_logger.warning.assert_called_once()
|
||||
@@ -245,14 +243,14 @@ async def test_update_group_pin_duty_pin_notify_false_pins_silent():
|
||||
context.application = MagicMock()
|
||||
|
||||
with patch.object(config, "DUTY_PIN_NOTIFY", False):
|
||||
with patch.object(mod, "_sync_get_message_id", return_value=4):
|
||||
with patch.object(mod, "_get_duty_message_text_sync", return_value="Text"):
|
||||
with patch.object(mod, "_get_next_shift_end_sync", return_value=None):
|
||||
with patch.object(
|
||||
mod, "_schedule_next_update", AsyncMock()
|
||||
) as mock_schedule:
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
await mod.update_group_pin(context)
|
||||
with patch.object(
|
||||
mod, "_sync_get_pin_refresh_data", return_value=(4, "Text", None)
|
||||
):
|
||||
with patch.object(
|
||||
mod, "_schedule_next_update", AsyncMock()
|
||||
) as mock_schedule:
|
||||
with patch.object(mod, "_sync_save_pin") as mock_save:
|
||||
await mod.update_group_pin(context)
|
||||
context.bot.send_message.assert_called_once_with(chat_id=333, text="Text")
|
||||
context.bot.unpin_chat_message.assert_called_once_with(chat_id=333)
|
||||
context.bot.pin_chat_message.assert_called_once_with(
|
||||
|
||||
Reference in New Issue
Block a user