Skip to content

Commit 0d3c14c

Browse files
Add in_ and at helpers to allow for directly requesting a retry
1 parent 0244315 commit 0d3c14c

File tree

3 files changed

+136
-1
lines changed

3 files changed

+136
-1
lines changed

src/docket/dependencies.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
from contextlib import AsyncExitStack, asynccontextmanager
55
from contextvars import ContextVar
6-
from datetime import timedelta
6+
from datetime import datetime, timedelta
77
from types import TracebackType
88
from typing import (
99
TYPE_CHECKING,
@@ -14,11 +14,13 @@
1414
Callable,
1515
Counter,
1616
Generic,
17+
NoReturn,
1718
TypeVar,
1819
cast,
1920
)
2021

2122
from .docket import Docket
23+
from .exceptions import ForcedRetry
2224
from .execution import Execution, TaskFunction, get_signature
2325

2426
if TYPE_CHECKING: # pragma: no cover
@@ -222,6 +224,17 @@ async def __aenter__(self) -> "Retry":
222224
retry.attempt = execution.attempt
223225
return retry
224226

227+
def at(self, when: datetime) -> NoReturn:
228+
now = datetime.now()
229+
diff = when - now
230+
diff = diff if diff.total_seconds() >= 0 else timedelta(0)
231+
232+
self.in_(diff)
233+
234+
def in_(self, when: timedelta) -> NoReturn:
235+
self.delay = when
236+
raise ForcedRetry()
237+
225238

226239
class ExponentialRetry(Retry):
227240
"""Configures exponential retries for a task. You can specify the total number

src/docket/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ForcedRetry(Exception):
2+
"""Raised when a task requests a retry via `in_` or `at`"""

tests/test_dependencies.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import datetime, timedelta
23

34
import pytest
45

@@ -95,6 +96,125 @@ async def the_task(
9596
assert calls == 2
9697

9798

99+
async def test_user_can_request_a_retry_in_timedelta_time(
100+
docket: Docket, worker: Worker
101+
):
102+
calls = 0
103+
first_call_time = None
104+
second_call_time = None
105+
106+
async def the_task(
107+
a: str,
108+
b: str,
109+
retry: Retry = Retry(attempts=2),
110+
):
111+
assert a == "a"
112+
assert b == "b"
113+
114+
nonlocal calls
115+
calls += 1
116+
117+
nonlocal first_call_time
118+
if not first_call_time:
119+
first_call_time = datetime.now()
120+
retry.in_(timedelta(seconds=0.5))
121+
else:
122+
nonlocal second_call_time
123+
second_call_time = datetime.now()
124+
125+
await docket.add(the_task)("a", "b")
126+
127+
await worker.run_until_finished()
128+
129+
assert calls == 2
130+
131+
assert isinstance(first_call_time, datetime)
132+
assert isinstance(second_call_time, datetime)
133+
134+
delay = second_call_time - first_call_time
135+
assert delay.total_seconds() > 0 < 1
136+
137+
138+
async def test_user_can_request_a_retry_at_a_specific_time(
139+
docket: Docket, worker: Worker
140+
):
141+
calls = 0
142+
first_call_time = None
143+
second_call_time = None
144+
145+
async def the_task(
146+
a: str,
147+
b: str,
148+
retry: Retry = Retry(attempts=2),
149+
):
150+
assert a == "a"
151+
assert b == "b"
152+
153+
nonlocal calls
154+
calls += 1
155+
156+
nonlocal first_call_time
157+
if not first_call_time:
158+
when = datetime.now() + timedelta(seconds=0.5)
159+
first_call_time = datetime.now()
160+
retry.at(when)
161+
else:
162+
nonlocal second_call_time
163+
second_call_time = datetime.now()
164+
165+
await docket.add(the_task)("a", "b")
166+
167+
await worker.run_until_finished()
168+
169+
assert calls == 2
170+
171+
assert isinstance(first_call_time, datetime)
172+
assert isinstance(second_call_time, datetime)
173+
174+
delay = second_call_time - first_call_time
175+
assert delay.total_seconds() > 0 < 1
176+
177+
178+
async def test_user_can_request_a_retry_at_a_specific_time_in_the_past(
179+
docket: Docket, worker: Worker
180+
):
181+
calls = 0
182+
first_call_time = None
183+
second_call_time = None
184+
185+
async def the_task(
186+
a: str,
187+
b: str,
188+
retry: Retry = Retry(attempts=2),
189+
):
190+
assert a == "a"
191+
assert b == "b"
192+
193+
nonlocal calls
194+
calls += 1
195+
196+
nonlocal first_call_time
197+
if not first_call_time:
198+
when = datetime.now() - timedelta(days=1)
199+
first_call_time = datetime.now()
200+
retry.at(when)
201+
else:
202+
nonlocal second_call_time
203+
second_call_time = datetime.now()
204+
205+
await docket.add(the_task)("a", "b")
206+
207+
await worker.run_until_finished()
208+
209+
assert calls == 2
210+
211+
assert isinstance(first_call_time, datetime)
212+
assert isinstance(second_call_time, datetime)
213+
214+
delay = second_call_time - first_call_time
215+
assert delay.total_seconds() > 0 < 1
216+
217+
98218
async def test_dependencies_error_for_missing_task_argument(
99219
docket: Docket, worker: Worker, caplog: pytest.LogCaptureFixture
100220
):

0 commit comments

Comments
 (0)