Skip to content

Commit 2cd766a

Browse files
authored
Merge pull request #7 from modern-python/routers-in-init
allow routers in Timers init, more tests
2 parents 2cbffbd + f9498b1 commit 2cd766a

File tree

2 files changed

+98
-9
lines changed

2 files changed

+98
-9
lines changed

redis_timers/timers.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import contextlib
3-
import dataclasses
43
import datetime
54
import logging
65
import random
@@ -26,11 +25,21 @@
2625
TIMEZONE = zoneinfo.ZoneInfo("UTC")
2726

2827

29-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
3028
class Timers:
31-
redis_client: "aioredis.Redis[str]"
32-
context: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
33-
handlers_by_topics: dict[str, Handler[typing.Any]] = dataclasses.field(default_factory=dict, init=False)
29+
__slots__ = "context", "handlers_by_topics", "redis_client"
30+
31+
def __init__(
32+
self,
33+
*,
34+
redis_client: "aioredis.Redis[str]",
35+
context: dict[str, typing.Any],
36+
routers: list[Router] | None = None,
37+
) -> None:
38+
self.handlers_by_topics: dict[str, Handler[typing.Any]] = {}
39+
self.redis_client = redis_client
40+
self.context = context
41+
if routers:
42+
self.include_routers(*routers)
3443

3544
def include_router(self, router: Router) -> None:
3645
for h in router.handlers:
@@ -83,14 +92,15 @@ async def handle_ready_timers(self) -> None:
8392
key=timer_key,
8493
)
8594
if await lock.locked():
95+
logger.debug(f"Timer is locked, {timer_key=}")
8696
continue
8797

8898
with contextlib.suppress(LockError):
8999
async with lock:
90100
await self._handle_one_timer(timer_key)
91101
await self._remove_timer_by_key(timer_key)
92102

93-
async def run_forever(self) -> None:
103+
async def run_forever(self) -> None: # pragma: no cover
94104
while True:
95105
try:
96106
await self.handle_ready_timers()

tests/test_timers.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import logging
23
import os
34
import typing
45
from collections.abc import AsyncGenerator
@@ -7,7 +8,7 @@
78
import pytest
89
from redis import asyncio as aioredis
910

10-
from redis_timers import Timers, settings
11+
from redis_timers import Timers, consume_lock, settings
1112
from redis_timers.router import Router
1213

1314

@@ -70,8 +71,7 @@ async def another_handler(data: AnotherPayloadModel, context: dict[str, typing.A
7071
handler_results.add_result(data)
7172
assert context["some_key"] == "some_value"
7273

73-
timers = Timers(redis_client=redis_client, context={"some_key": "some_value"})
74-
timers.include_router(router1)
74+
timers = Timers(redis_client=redis_client, context={"some_key": "some_value"}, routers=[router1])
7575
timers.include_routers(router2)
7676

7777
return timers
@@ -104,6 +104,17 @@ async def test_set_and_remove_timer(timers_instance: Timers) -> None:
104104
assert not payloads_dict
105105

106106

107+
async def test_set_undefined_timer(timers_instance: Timers) -> None:
108+
payload = SomePayloadModel(message="test", count=1)
109+
with pytest.raises(RuntimeError, match="Handler is not found, topic='wrong_topic'"):
110+
await timers_instance.set_timer(
111+
topic="wrong_topic",
112+
timer_id="test_timer_1",
113+
payload=payload,
114+
activation_period=datetime.timedelta(seconds=1),
115+
)
116+
117+
107118
async def test_handle_ready_timers(timers_instance: Timers, handler_results: HandlerResults) -> None:
108119
payload = SomePayloadModel(message="ready_timer", count=42)
109120
await timers_instance.set_timer(
@@ -129,6 +140,74 @@ async def test_handle_ready_timers(timers_instance: Timers, handler_results: Han
129140
assert not payloads_dict
130141

131142

143+
async def test_handle_ready_timer_no_handler(timers_instance: Timers, caplog: pytest.LogCaptureFixture) -> None:
144+
caplog.set_level(logging.INFO)
145+
payload = SomePayloadModel(message="ready_timer", count=42)
146+
await timers_instance.set_timer(
147+
topic="some_topic",
148+
timer_id="ready_timer_1",
149+
payload=payload,
150+
activation_period=datetime.timedelta(seconds=0), # Ready immediately
151+
)
152+
del timers_instance.handlers_by_topics["some_topic"]
153+
await timers_instance.handle_ready_timers()
154+
155+
assert len(caplog.records) == 1
156+
assert "Handler is not found" in caplog.records[0].message
157+
158+
159+
async def test_handle_ready_timer_no_payload(timers_instance: Timers, caplog: pytest.LogCaptureFixture) -> None:
160+
caplog.set_level(logging.INFO)
161+
payload = SomePayloadModel(message="ready_timer", count=42)
162+
await timers_instance.set_timer(
163+
topic="some_topic",
164+
timer_id="ready_timer_1",
165+
payload=payload,
166+
activation_period=datetime.timedelta(seconds=0), # Ready immediately
167+
)
168+
await timers_instance.redis_client.hdel(settings.TIMERS_PAYLOADS_KEY, "some_topic--ready_timer_1")
169+
await timers_instance.handle_ready_timers()
170+
171+
assert len(caplog.records) == 1
172+
assert "No payload found" in caplog.records[0].message
173+
174+
175+
async def test_handle_ready_timer_invalid_payload(timers_instance: Timers, caplog: pytest.LogCaptureFixture) -> None:
176+
caplog.set_level(logging.INFO)
177+
payload = SomePayloadModel(message="ready_timer", count=42)
178+
await timers_instance.set_timer(
179+
topic="some_topic",
180+
timer_id="ready_timer_1",
181+
payload=payload,
182+
activation_period=datetime.timedelta(seconds=0), # Ready immediately
183+
)
184+
await timers_instance.redis_client.hset(settings.TIMERS_PAYLOADS_KEY, "some_topic--ready_timer_1", "{}")
185+
await timers_instance.handle_ready_timers()
186+
187+
assert len(caplog.records) == 1
188+
assert "Failed to parse payload" in caplog.records[0].message
189+
190+
191+
async def test_handle_ready_timer_locked(timers_instance: Timers, caplog: pytest.LogCaptureFixture) -> None:
192+
caplog.set_level(logging.DEBUG)
193+
payload = SomePayloadModel(message="ready_timer", count=42)
194+
await timers_instance.set_timer(
195+
topic="some_topic",
196+
timer_id="ready_timer_1",
197+
payload=payload,
198+
activation_period=datetime.timedelta(seconds=0), # Ready immediately
199+
)
200+
lock = consume_lock(
201+
redis_client=timers_instance.redis_client,
202+
key="some_topic--ready_timer_1",
203+
)
204+
async with lock:
205+
await timers_instance.handle_ready_timers()
206+
207+
assert len(caplog.records) == 1
208+
assert "Timer is locked" in caplog.records[0].message
209+
210+
132211
async def test_handle_multiple_ready_timers(timers_instance: Timers, handler_results: HandlerResults) -> None:
133212
payload1 = SomePayloadModel(message="timer_1", count=1)
134213
payload2 = AnotherPayloadModel(value="timer_2")

0 commit comments

Comments
 (0)