backon 3.3.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backon-3.3.0 → backon-3.4.0}/PKG-INFO +1 -1
- {backon-3.3.0 → backon-3.4.0}/backon/__init__.py +2 -1
- {backon-3.3.0 → backon-3.4.0}/backon/_decorator.py +18 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_retry.py +163 -52
- {backon-3.3.0 → backon-3.4.0}/backon/_state.py +42 -0
- {backon-3.3.0 → backon-3.4.0}/pyproject.toml +1 -1
- {backon-3.3.0 → backon-3.4.0}/tests/test_backon.py +1 -0
- backon-3.4.0/tests/test_retry_call_state.py +292 -0
- {backon-3.3.0 → backon-3.4.0}/LICENSE +0 -0
- {backon-3.3.0 → backon-3.4.0}/README.md +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_async.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_common.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_conditions.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_jitter.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_sync.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_typing.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/_wait_gen.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/py.typed +0 -0
- {backon-3.3.0 → backon-3.4.0}/backon/types.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/__init__.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_advanced_features.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_backon_async.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_backon_predicate.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_backon_sync.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_features.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_jitter.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_retry.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_types.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_typing.py +0 -0
- {backon-3.3.0 → backon-3.4.0}/tests/test_wait_gen.py +0 -0
|
@@ -25,7 +25,7 @@ from backon._conditions import (
|
|
|
25
25
|
from backon._decorator import on_exception, on_predicate
|
|
26
26
|
from backon._jitter import full_jitter, random_jitter
|
|
27
27
|
from backon._retry import Retrying, retry, sleep_using_event
|
|
28
|
-
from backon._state import RetryError, RetryState, TryAgain
|
|
28
|
+
from backon._state import RetryCallState, RetryError, RetryState, TryAgain
|
|
29
29
|
from backon._wait_gen import (
|
|
30
30
|
constant,
|
|
31
31
|
decay,
|
|
@@ -66,6 +66,7 @@ __all__ = [
|
|
|
66
66
|
"TryAgain",
|
|
67
67
|
"RetryError",
|
|
68
68
|
"RetryState",
|
|
69
|
+
"RetryCallState",
|
|
69
70
|
"Stop",
|
|
70
71
|
"RetryCondition",
|
|
71
72
|
"stop_after_attempt",
|
|
@@ -54,10 +54,13 @@ def on_predicate(
|
|
|
54
54
|
retry_error_callback: Callable[[dict], Any] | None = None,
|
|
55
55
|
raise_on_giveup: bool = True,
|
|
56
56
|
sleep: Callable[[float], Any] | None = None,
|
|
57
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
58
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
57
59
|
**wait_gen_kwargs: Any,
|
|
58
60
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
59
61
|
def decorate(target: Callable[P, R]) -> Callable[P, R]:
|
|
60
62
|
nonlocal logger, on_success, on_backoff, on_giveup, on_attempt, before_sleep
|
|
63
|
+
nonlocal before, after
|
|
61
64
|
|
|
62
65
|
logger = _prepare_logger(logger)
|
|
63
66
|
on_success = _config_handlers(on_success)
|
|
@@ -75,6 +78,8 @@ def on_predicate(
|
|
|
75
78
|
)
|
|
76
79
|
on_attempt = _config_handlers(on_attempt)
|
|
77
80
|
before_sleep = _config_handlers(before_sleep)
|
|
81
|
+
before = _config_handlers(before)
|
|
82
|
+
after = _config_handlers(after)
|
|
78
83
|
|
|
79
84
|
condition: RetryCondition = retry_if_result(predicate)
|
|
80
85
|
|
|
@@ -107,6 +112,8 @@ def on_predicate(
|
|
|
107
112
|
retry_error_callback=retry_error_callback,
|
|
108
113
|
raise_on_giveup=raise_on_giveup,
|
|
109
114
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
115
|
+
before=before,
|
|
116
|
+
after=after,
|
|
110
117
|
),
|
|
111
118
|
)
|
|
112
119
|
|
|
@@ -137,6 +144,8 @@ def on_predicate(
|
|
|
137
144
|
retry_error_callback=retry_error_callback,
|
|
138
145
|
raise_on_giveup=raise_on_giveup,
|
|
139
146
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
147
|
+
before=before,
|
|
148
|
+
after=after,
|
|
140
149
|
),
|
|
141
150
|
)
|
|
142
151
|
|
|
@@ -164,10 +173,13 @@ def on_exception(
|
|
|
164
173
|
backoff_log_level: int = logging.INFO,
|
|
165
174
|
giveup_log_level: int = logging.ERROR,
|
|
166
175
|
sleep: Callable[[float], Any] | None = None,
|
|
176
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
177
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
167
178
|
**wait_gen_kwargs: Any,
|
|
168
179
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
169
180
|
def decorate(target: Callable[P, R]) -> Callable[P, R]:
|
|
170
181
|
nonlocal logger, on_success, on_backoff, on_giveup, on_attempt, before_sleep
|
|
182
|
+
nonlocal before, after
|
|
171
183
|
|
|
172
184
|
logger = _prepare_logger(logger)
|
|
173
185
|
on_success = _config_handlers(on_success)
|
|
@@ -185,6 +197,8 @@ def on_exception(
|
|
|
185
197
|
)
|
|
186
198
|
on_attempt = _config_handlers(on_attempt)
|
|
187
199
|
before_sleep = _config_handlers(before_sleep)
|
|
200
|
+
before = _config_handlers(before)
|
|
201
|
+
after = _config_handlers(after)
|
|
188
202
|
|
|
189
203
|
exc_types: tuple[type[Exception], ...]
|
|
190
204
|
if isinstance(exception, type):
|
|
@@ -233,6 +247,8 @@ def on_exception(
|
|
|
233
247
|
retry_error_callback=retry_error_callback,
|
|
234
248
|
raise_on_giveup=raise_on_giveup,
|
|
235
249
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
250
|
+
before=before,
|
|
251
|
+
after=after,
|
|
236
252
|
),
|
|
237
253
|
)
|
|
238
254
|
|
|
@@ -263,6 +279,8 @@ def on_exception(
|
|
|
263
279
|
retry_error_callback=retry_error_callback,
|
|
264
280
|
raise_on_giveup=raise_on_giveup,
|
|
265
281
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
282
|
+
before=before,
|
|
283
|
+
after=after,
|
|
266
284
|
),
|
|
267
285
|
)
|
|
268
286
|
|
|
@@ -30,7 +30,7 @@ from backon._conditions import (
|
|
|
30
30
|
stop_never,
|
|
31
31
|
)
|
|
32
32
|
from backon._jitter import full_jitter
|
|
33
|
-
from backon._state import Attempt, RetryState, TryAgain
|
|
33
|
+
from backon._state import Attempt, RetryCallState, RetryState, TryAgain
|
|
34
34
|
from backon._typing import (
|
|
35
35
|
_Handler,
|
|
36
36
|
_Jitterer,
|
|
@@ -123,25 +123,37 @@ def _retry_loop_sync(
|
|
|
123
123
|
raise_on_giveup,
|
|
124
124
|
max_time,
|
|
125
125
|
wait_gen_kwargs,
|
|
126
|
+
before=None,
|
|
127
|
+
after=None,
|
|
128
|
+
_holder=None,
|
|
126
129
|
):
|
|
127
130
|
state = RetryState(target=target)
|
|
128
131
|
start_time = _now()
|
|
129
132
|
state.start_time = start_time
|
|
133
|
+
call_state = RetryCallState(fn=target, start_time=start_time)
|
|
134
|
+
if _holder is not None:
|
|
135
|
+
_holder["state"] = state
|
|
136
|
+
_holder["call_state"] = call_state
|
|
130
137
|
wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
131
138
|
|
|
132
139
|
while True:
|
|
133
140
|
state.tries += 1
|
|
134
141
|
state.elapsed = _now() - start_time
|
|
142
|
+
call_state.attempt_number = state.tries
|
|
135
143
|
outcome = Attempt(tries=state.tries, elapsed=state.elapsed)
|
|
136
144
|
|
|
137
145
|
_call_hdlrs(on_attempt, state.to_details())
|
|
138
146
|
|
|
147
|
+
_call_hdlrs(before, state.to_details())
|
|
148
|
+
|
|
139
149
|
try:
|
|
140
150
|
ret = target()
|
|
141
151
|
except TryAgain:
|
|
142
152
|
outcome.exception = None
|
|
143
153
|
outcome.value = None
|
|
144
154
|
state.outcome = outcome
|
|
155
|
+
call_state.outcome = outcome
|
|
156
|
+
call_state.outcome_timestamp = _now()
|
|
145
157
|
try:
|
|
146
158
|
value = wait.send(None)
|
|
147
159
|
if jitter is not None:
|
|
@@ -166,6 +178,10 @@ def _retry_loop_sync(
|
|
|
166
178
|
outcome.value = None
|
|
167
179
|
state.outcome = outcome
|
|
168
180
|
state.idle_for += state.elapsed
|
|
181
|
+
call_state.outcome = outcome
|
|
182
|
+
call_state.outcome_timestamp = _now()
|
|
183
|
+
call_state.idle_for += call_state.elapsed
|
|
184
|
+
_call_hdlrs(after, state.to_details())
|
|
169
185
|
|
|
170
186
|
if not condition(state):
|
|
171
187
|
details = state.to_details()
|
|
@@ -203,6 +219,7 @@ def _retry_loop_sync(
|
|
|
203
219
|
raise exc from None
|
|
204
220
|
return None
|
|
205
221
|
|
|
222
|
+
call_state.upcoming_sleep = seconds
|
|
206
223
|
details = state.to_details()
|
|
207
224
|
details["wait"] = seconds
|
|
208
225
|
details["exception"] = exc
|
|
@@ -210,10 +227,14 @@ def _retry_loop_sync(
|
|
|
210
227
|
_call_hdlrs(on_backoff, details)
|
|
211
228
|
if seconds > 0:
|
|
212
229
|
sleep(seconds)
|
|
230
|
+
call_state.idle_for = call_state.idle_for + seconds
|
|
213
231
|
else:
|
|
214
232
|
outcome.value = ret
|
|
215
233
|
outcome.exception = None
|
|
216
234
|
state.outcome = outcome
|
|
235
|
+
call_state.outcome = outcome
|
|
236
|
+
call_state.outcome_timestamp = _now()
|
|
237
|
+
_call_hdlrs(after, state.to_details())
|
|
217
238
|
|
|
218
239
|
if condition(state):
|
|
219
240
|
if stop(state):
|
|
@@ -236,6 +257,7 @@ def _retry_loop_sync(
|
|
|
236
257
|
_call_hdlrs(on_giveup, details)
|
|
237
258
|
return ret
|
|
238
259
|
|
|
260
|
+
call_state.upcoming_sleep = seconds
|
|
239
261
|
details = state.to_details()
|
|
240
262
|
details["wait"] = seconds
|
|
241
263
|
details["value"] = ret
|
|
@@ -243,6 +265,7 @@ def _retry_loop_sync(
|
|
|
243
265
|
_call_hdlrs(on_backoff, details)
|
|
244
266
|
if seconds > 0:
|
|
245
267
|
sleep(seconds)
|
|
268
|
+
call_state.idle_for = call_state.idle_for + seconds
|
|
246
269
|
else:
|
|
247
270
|
details = state.to_details()
|
|
248
271
|
details["value"] = ret
|
|
@@ -266,25 +289,37 @@ async def _retry_loop_async(
|
|
|
266
289
|
raise_on_giveup,
|
|
267
290
|
max_time,
|
|
268
291
|
wait_gen_kwargs,
|
|
292
|
+
before=None,
|
|
293
|
+
after=None,
|
|
294
|
+
_holder=None,
|
|
269
295
|
):
|
|
270
296
|
state = RetryState(target=target)
|
|
271
297
|
start_time = _now()
|
|
272
298
|
state.start_time = start_time
|
|
299
|
+
call_state = RetryCallState(fn=target, start_time=start_time)
|
|
300
|
+
if _holder is not None:
|
|
301
|
+
_holder["state"] = state
|
|
302
|
+
_holder["call_state"] = call_state
|
|
273
303
|
wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
274
304
|
|
|
275
305
|
while True:
|
|
276
306
|
state.tries += 1
|
|
277
307
|
state.elapsed = _now() - start_time
|
|
308
|
+
call_state.attempt_number = state.tries
|
|
278
309
|
outcome = Attempt(tries=state.tries, elapsed=state.elapsed)
|
|
279
310
|
|
|
280
311
|
await _call_hdlrs_async(on_attempt, state.to_details())
|
|
281
312
|
|
|
313
|
+
_call_hdlrs(before, state.to_details())
|
|
314
|
+
|
|
282
315
|
try:
|
|
283
316
|
ret = await target()
|
|
284
317
|
except TryAgain:
|
|
285
318
|
outcome.exception = None
|
|
286
319
|
outcome.value = None
|
|
287
320
|
state.outcome = outcome
|
|
321
|
+
call_state.outcome = outcome
|
|
322
|
+
call_state.outcome_timestamp = _now()
|
|
288
323
|
try:
|
|
289
324
|
value = wait.send(None)
|
|
290
325
|
if jitter is not None:
|
|
@@ -309,6 +344,10 @@ async def _retry_loop_async(
|
|
|
309
344
|
outcome.value = None
|
|
310
345
|
state.outcome = outcome
|
|
311
346
|
state.idle_for += state.elapsed
|
|
347
|
+
call_state.outcome = outcome
|
|
348
|
+
call_state.outcome_timestamp = _now()
|
|
349
|
+
call_state.idle_for += call_state.elapsed
|
|
350
|
+
await _call_hdlrs_async(after, state.to_details())
|
|
312
351
|
|
|
313
352
|
if not condition(state):
|
|
314
353
|
details = state.to_details()
|
|
@@ -346,6 +385,7 @@ async def _retry_loop_async(
|
|
|
346
385
|
raise exc from None
|
|
347
386
|
return None
|
|
348
387
|
|
|
388
|
+
call_state.upcoming_sleep = seconds
|
|
349
389
|
details = state.to_details()
|
|
350
390
|
details["wait"] = seconds
|
|
351
391
|
details["exception"] = exc
|
|
@@ -353,10 +393,14 @@ async def _retry_loop_async(
|
|
|
353
393
|
await _call_hdlrs_async(on_backoff, details)
|
|
354
394
|
if seconds > 0:
|
|
355
395
|
await sleep(seconds)
|
|
396
|
+
call_state.idle_for = call_state.idle_for + seconds
|
|
356
397
|
else:
|
|
357
398
|
outcome.value = ret
|
|
358
399
|
outcome.exception = None
|
|
359
400
|
state.outcome = outcome
|
|
401
|
+
call_state.outcome = outcome
|
|
402
|
+
call_state.outcome_timestamp = _now()
|
|
403
|
+
await _call_hdlrs_async(after, state.to_details())
|
|
360
404
|
|
|
361
405
|
if condition(state):
|
|
362
406
|
if stop(state):
|
|
@@ -379,6 +423,7 @@ async def _retry_loop_async(
|
|
|
379
423
|
await _call_hdlrs_async(on_giveup, details)
|
|
380
424
|
return ret
|
|
381
425
|
|
|
426
|
+
call_state.upcoming_sleep = seconds
|
|
382
427
|
details = state.to_details()
|
|
383
428
|
details["wait"] = seconds
|
|
384
429
|
details["value"] = ret
|
|
@@ -386,6 +431,7 @@ async def _retry_loop_async(
|
|
|
386
431
|
await _call_hdlrs_async(on_backoff, details)
|
|
387
432
|
if seconds > 0:
|
|
388
433
|
await sleep(seconds)
|
|
434
|
+
call_state.idle_for = call_state.idle_for + seconds
|
|
389
435
|
else:
|
|
390
436
|
details = state.to_details()
|
|
391
437
|
details["value"] = ret
|
|
@@ -411,6 +457,9 @@ def _retry_sync_inner(
|
|
|
411
457
|
max_time=None,
|
|
412
458
|
wait_gen_kwargs=None,
|
|
413
459
|
max_tries=None,
|
|
460
|
+
before=None,
|
|
461
|
+
after=None,
|
|
462
|
+
_holder=None,
|
|
414
463
|
):
|
|
415
464
|
if not is_enabled():
|
|
416
465
|
return target()
|
|
@@ -437,6 +486,9 @@ def _retry_sync_inner(
|
|
|
437
486
|
raise_on_giveup,
|
|
438
487
|
max_time,
|
|
439
488
|
wait_gen_kwargs,
|
|
489
|
+
before=before,
|
|
490
|
+
after=after,
|
|
491
|
+
_holder=_holder,
|
|
440
492
|
)
|
|
441
493
|
|
|
442
494
|
|
|
@@ -458,6 +510,9 @@ async def _retry_async_inner(
|
|
|
458
510
|
max_time=None,
|
|
459
511
|
wait_gen_kwargs=None,
|
|
460
512
|
max_tries=None,
|
|
513
|
+
before=None,
|
|
514
|
+
after=None,
|
|
515
|
+
_holder=None,
|
|
461
516
|
):
|
|
462
517
|
if not is_enabled():
|
|
463
518
|
return await target()
|
|
@@ -484,6 +539,9 @@ async def _retry_async_inner(
|
|
|
484
539
|
raise_on_giveup,
|
|
485
540
|
max_time,
|
|
486
541
|
wait_gen_kwargs,
|
|
542
|
+
before=before,
|
|
543
|
+
after=after,
|
|
544
|
+
_holder=_holder,
|
|
487
545
|
)
|
|
488
546
|
|
|
489
547
|
|
|
@@ -511,6 +569,9 @@ def _retry_sync(
|
|
|
511
569
|
giveup_log_level: int = logging.ERROR,
|
|
512
570
|
sleep: Callable[[float], Any] | None = None,
|
|
513
571
|
wait_gen_kwargs: dict | None = None,
|
|
572
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
573
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
574
|
+
_holder: dict | None = None,
|
|
514
575
|
) -> Any:
|
|
515
576
|
if wait_gen_kwargs is None:
|
|
516
577
|
wait_gen_kwargs = {}
|
|
@@ -533,6 +594,8 @@ def _retry_sync(
|
|
|
533
594
|
)
|
|
534
595
|
on_attempt = _config_handlers(on_attempt)
|
|
535
596
|
before_sleep = _config_handlers(before_sleep)
|
|
597
|
+
before = _config_handlers(before)
|
|
598
|
+
after = _config_handlers(after)
|
|
536
599
|
|
|
537
600
|
if condition is None:
|
|
538
601
|
condition = _make_default_condition(exception, giveup, predicate)
|
|
@@ -557,6 +620,9 @@ def _retry_sync(
|
|
|
557
620
|
raise_on_giveup,
|
|
558
621
|
max_time,
|
|
559
622
|
wait_gen_kwargs,
|
|
623
|
+
before=before,
|
|
624
|
+
after=after,
|
|
625
|
+
_holder=_holder,
|
|
560
626
|
)
|
|
561
627
|
|
|
562
628
|
|
|
@@ -584,6 +650,9 @@ async def _retry_async(
|
|
|
584
650
|
giveup_log_level: int = logging.ERROR,
|
|
585
651
|
sleep: Callable[[float], Any] | None = None,
|
|
586
652
|
wait_gen_kwargs: dict | None = None,
|
|
653
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
654
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
655
|
+
_holder: dict | None = None,
|
|
587
656
|
) -> Any:
|
|
588
657
|
if wait_gen_kwargs is None:
|
|
589
658
|
wait_gen_kwargs = {}
|
|
@@ -606,6 +675,8 @@ async def _retry_async(
|
|
|
606
675
|
)
|
|
607
676
|
on_attempt = _config_handlers(on_attempt)
|
|
608
677
|
before_sleep = _config_handlers(before_sleep)
|
|
678
|
+
before = _config_handlers(before)
|
|
679
|
+
after = _config_handlers(after)
|
|
609
680
|
|
|
610
681
|
if condition is None:
|
|
611
682
|
condition = _make_default_condition(exception, giveup, predicate)
|
|
@@ -630,6 +701,9 @@ async def _retry_async(
|
|
|
630
701
|
raise_on_giveup,
|
|
631
702
|
max_time,
|
|
632
703
|
wait_gen_kwargs,
|
|
704
|
+
before=before,
|
|
705
|
+
after=after,
|
|
706
|
+
_holder=_holder,
|
|
633
707
|
)
|
|
634
708
|
|
|
635
709
|
|
|
@@ -657,6 +731,8 @@ def retry(
|
|
|
657
731
|
giveup_log_level: int = logging.ERROR,
|
|
658
732
|
sleep: Callable[[float], Any] | None = None,
|
|
659
733
|
name: str = "",
|
|
734
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
735
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
660
736
|
**wait_gen_kwargs: Any,
|
|
661
737
|
) -> Any:
|
|
662
738
|
if inspect.iscoroutinefunction(target):
|
|
@@ -683,6 +759,8 @@ def retry(
|
|
|
683
759
|
giveup_log_level=giveup_log_level,
|
|
684
760
|
sleep=sleep,
|
|
685
761
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
762
|
+
before=before,
|
|
763
|
+
after=after,
|
|
686
764
|
)
|
|
687
765
|
return _retry_sync(
|
|
688
766
|
target,
|
|
@@ -707,6 +785,8 @@ def retry(
|
|
|
707
785
|
giveup_log_level=giveup_log_level,
|
|
708
786
|
sleep=sleep,
|
|
709
787
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
788
|
+
before=before,
|
|
789
|
+
after=after,
|
|
710
790
|
)
|
|
711
791
|
|
|
712
792
|
|
|
@@ -763,6 +843,8 @@ class Retrying:
|
|
|
763
843
|
sleep: Callable[[float], Any] | None = None,
|
|
764
844
|
enabled: bool = True,
|
|
765
845
|
name: str = "",
|
|
846
|
+
before: _Handler | Iterable[_Handler] | None = None,
|
|
847
|
+
after: _Handler | Iterable[_Handler] | None = None,
|
|
766
848
|
**wait_gen_kwargs: Any,
|
|
767
849
|
):
|
|
768
850
|
self._wait_gen = wait_gen
|
|
@@ -787,14 +869,23 @@ class Retrying:
|
|
|
787
869
|
self._sleep = sleep
|
|
788
870
|
self._enabled = enabled
|
|
789
871
|
self._name = name
|
|
872
|
+
self._before = before
|
|
873
|
+
self._after = after
|
|
790
874
|
self._wait_gen_kwargs = wait_gen_kwargs
|
|
791
875
|
self._state: RetryState | None = None
|
|
876
|
+
self._call_state: RetryCallState | None = None
|
|
792
877
|
|
|
793
878
|
@property
|
|
794
879
|
def statistics(self) -> dict:
|
|
795
|
-
if self.
|
|
796
|
-
return
|
|
797
|
-
|
|
880
|
+
if self._call_state is not None:
|
|
881
|
+
return self._call_state.statistics
|
|
882
|
+
if self._state is not None:
|
|
883
|
+
return self._state.statistics
|
|
884
|
+
return {}
|
|
885
|
+
|
|
886
|
+
@property
|
|
887
|
+
def call_state(self) -> RetryCallState | None:
|
|
888
|
+
return self._call_state
|
|
798
889
|
|
|
799
890
|
@property
|
|
800
891
|
def enabled(self) -> bool:
|
|
@@ -903,6 +994,8 @@ class Retrying:
|
|
|
903
994
|
sleep=self._sleep,
|
|
904
995
|
enabled=self._enabled,
|
|
905
996
|
name=self._name,
|
|
997
|
+
before=self._before,
|
|
998
|
+
after=self._after,
|
|
906
999
|
**self._wait_gen_kwargs,
|
|
907
1000
|
)
|
|
908
1001
|
|
|
@@ -916,30 +1009,39 @@ class Retrying:
|
|
|
916
1009
|
def wrapped():
|
|
917
1010
|
return target(*args, **kwargs)
|
|
918
1011
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1012
|
+
_holder: dict = {}
|
|
1013
|
+
try:
|
|
1014
|
+
result = _retry_sync(
|
|
1015
|
+
wrapped,
|
|
1016
|
+
self._wait_gen,
|
|
1017
|
+
predicate=self._predicate,
|
|
1018
|
+
exception=self._exception,
|
|
1019
|
+
max_tries=self._max_tries,
|
|
1020
|
+
max_time=self._max_time,
|
|
1021
|
+
jitter=self._jitter,
|
|
1022
|
+
giveup=self._giveup,
|
|
1023
|
+
condition=self._condition,
|
|
1024
|
+
stop=self._stop,
|
|
1025
|
+
on_success=self._on_success,
|
|
1026
|
+
on_backoff=self._on_backoff,
|
|
1027
|
+
on_giveup=self._on_giveup,
|
|
1028
|
+
on_attempt=self._on_attempt,
|
|
1029
|
+
before_sleep=self._before_sleep,
|
|
1030
|
+
retry_error_callback=self._retry_error_callback,
|
|
1031
|
+
raise_on_giveup=self._raise_on_giveup,
|
|
1032
|
+
logger=self._logger,
|
|
1033
|
+
backoff_log_level=self._backoff_log_level,
|
|
1034
|
+
giveup_log_level=self._giveup_log_level,
|
|
1035
|
+
sleep=self._sleep,
|
|
1036
|
+
wait_gen_kwargs=self._wait_gen_kwargs,
|
|
1037
|
+
before=self._before,
|
|
1038
|
+
after=self._after,
|
|
1039
|
+
_holder=_holder,
|
|
1040
|
+
)
|
|
1041
|
+
return result
|
|
1042
|
+
finally:
|
|
1043
|
+
self._state = _holder.get("state")
|
|
1044
|
+
self._call_state = _holder.get("call_state")
|
|
943
1045
|
|
|
944
1046
|
async def async_call(
|
|
945
1047
|
self, target: Callable[..., Any], *args: Any, **kwargs: Any
|
|
@@ -947,30 +1049,39 @@ class Retrying:
|
|
|
947
1049
|
async def wrapped():
|
|
948
1050
|
return await target(*args, **kwargs)
|
|
949
1051
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1052
|
+
_holder: dict = {}
|
|
1053
|
+
try:
|
|
1054
|
+
result = await _retry_async(
|
|
1055
|
+
wrapped,
|
|
1056
|
+
self._wait_gen,
|
|
1057
|
+
predicate=self._predicate,
|
|
1058
|
+
exception=self._exception,
|
|
1059
|
+
max_tries=self._max_tries,
|
|
1060
|
+
max_time=self._max_time,
|
|
1061
|
+
jitter=self._jitter,
|
|
1062
|
+
giveup=self._giveup,
|
|
1063
|
+
condition=self._condition,
|
|
1064
|
+
stop=self._stop,
|
|
1065
|
+
on_success=self._on_success,
|
|
1066
|
+
on_backoff=self._on_backoff,
|
|
1067
|
+
on_giveup=self._on_giveup,
|
|
1068
|
+
on_attempt=self._on_attempt,
|
|
1069
|
+
before_sleep=self._before_sleep,
|
|
1070
|
+
retry_error_callback=self._retry_error_callback,
|
|
1071
|
+
raise_on_giveup=self._raise_on_giveup,
|
|
1072
|
+
logger=self._logger,
|
|
1073
|
+
backoff_log_level=self._backoff_log_level,
|
|
1074
|
+
giveup_log_level=self._giveup_log_level,
|
|
1075
|
+
sleep=self._sleep,
|
|
1076
|
+
wait_gen_kwargs=self._wait_gen_kwargs,
|
|
1077
|
+
before=self._before,
|
|
1078
|
+
after=self._after,
|
|
1079
|
+
_holder=_holder,
|
|
1080
|
+
)
|
|
1081
|
+
return result
|
|
1082
|
+
finally:
|
|
1083
|
+
self._state = _holder.get("state")
|
|
1084
|
+
self._call_state = _holder.get("call_state")
|
|
974
1085
|
|
|
975
1086
|
|
|
976
1087
|
def sleep_using_event(event) -> Callable[[float], None]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
|
+
import time as time_module
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from typing import Any
|
|
@@ -73,3 +74,44 @@ class RetryState:
|
|
|
73
74
|
"tries": self.tries,
|
|
74
75
|
"elapsed": self.elapsed,
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class RetryCallState:
|
|
81
|
+
fn: Callable[..., Any] | None = None
|
|
82
|
+
args: tuple = ()
|
|
83
|
+
kwargs: dict = field(default_factory=dict)
|
|
84
|
+
attempt_number: int = 1
|
|
85
|
+
outcome: Attempt | None = None
|
|
86
|
+
outcome_timestamp: float | None = None
|
|
87
|
+
start_time: float = 0.0
|
|
88
|
+
idle_for: float = 0.0
|
|
89
|
+
upcoming_sleep: float = 0.0
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def elapsed(self) -> float:
|
|
93
|
+
if self.start_time == 0:
|
|
94
|
+
return 0.0
|
|
95
|
+
return time_module.monotonic() - self.start_time
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def seconds_since_start(self) -> float:
|
|
99
|
+
return self.elapsed
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def statistics(self) -> dict[str, Any]:
|
|
103
|
+
return {
|
|
104
|
+
"start_time": self.start_time,
|
|
105
|
+
"attempt_number": self.attempt_number,
|
|
106
|
+
"idle_for": self.idle_for,
|
|
107
|
+
"elapsed": self.elapsed,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def to_details(self) -> dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"target": self.fn,
|
|
113
|
+
"args": self.args,
|
|
114
|
+
"kwargs": self.kwargs,
|
|
115
|
+
"tries": self.attempt_number,
|
|
116
|
+
"elapsed": self.elapsed,
|
|
117
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import backon
|
|
6
|
+
from backon._state import RetryCallState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestRetryCallState:
|
|
10
|
+
def test_elapsed_property(self):
|
|
11
|
+
state = RetryCallState(start_time=time.monotonic())
|
|
12
|
+
time.sleep(0.01)
|
|
13
|
+
assert state.elapsed > 0
|
|
14
|
+
|
|
15
|
+
def test_statistics(self):
|
|
16
|
+
state = RetryCallState(
|
|
17
|
+
fn=lambda: None,
|
|
18
|
+
attempt_number=3,
|
|
19
|
+
idle_for=2.5,
|
|
20
|
+
start_time=time.monotonic(),
|
|
21
|
+
)
|
|
22
|
+
stats = state.statistics
|
|
23
|
+
assert stats["attempt_number"] == 3
|
|
24
|
+
assert stats["idle_for"] == 2.5
|
|
25
|
+
assert "start_time" in stats
|
|
26
|
+
assert "elapsed" in stats
|
|
27
|
+
|
|
28
|
+
def test_to_details(self):
|
|
29
|
+
state = RetryCallState(
|
|
30
|
+
fn=lambda: None,
|
|
31
|
+
attempt_number=3,
|
|
32
|
+
start_time=time.monotonic(),
|
|
33
|
+
)
|
|
34
|
+
details = state.to_details()
|
|
35
|
+
assert details["tries"] == 3
|
|
36
|
+
assert details["target"] is state.fn
|
|
37
|
+
|
|
38
|
+
def test_zero_start_time_elapsed(self):
|
|
39
|
+
state = RetryCallState()
|
|
40
|
+
assert state.elapsed == 0.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestBeforeAfterHooks:
|
|
44
|
+
def test_before_and_after_with_success(self):
|
|
45
|
+
order = []
|
|
46
|
+
|
|
47
|
+
def before(details):
|
|
48
|
+
order.append("before")
|
|
49
|
+
|
|
50
|
+
def after(details):
|
|
51
|
+
order.append("after")
|
|
52
|
+
|
|
53
|
+
@backon.on_exception(
|
|
54
|
+
backon.constant,
|
|
55
|
+
ValueError,
|
|
56
|
+
max_tries=3,
|
|
57
|
+
jitter=None,
|
|
58
|
+
interval=0.01,
|
|
59
|
+
before=before,
|
|
60
|
+
after=after,
|
|
61
|
+
)
|
|
62
|
+
def f():
|
|
63
|
+
return "ok"
|
|
64
|
+
|
|
65
|
+
f()
|
|
66
|
+
assert order == ["before", "after"]
|
|
67
|
+
|
|
68
|
+
def test_before_and_after_with_exception(self):
|
|
69
|
+
order = []
|
|
70
|
+
|
|
71
|
+
def before(details):
|
|
72
|
+
order.append("before")
|
|
73
|
+
|
|
74
|
+
def after(details):
|
|
75
|
+
order.append("after")
|
|
76
|
+
|
|
77
|
+
@backon.on_exception(
|
|
78
|
+
backon.constant,
|
|
79
|
+
ValueError,
|
|
80
|
+
max_tries=2,
|
|
81
|
+
jitter=None,
|
|
82
|
+
interval=0.01,
|
|
83
|
+
before=before,
|
|
84
|
+
after=after,
|
|
85
|
+
raise_on_giveup=True,
|
|
86
|
+
)
|
|
87
|
+
def f():
|
|
88
|
+
raise ValueError("fail")
|
|
89
|
+
|
|
90
|
+
with pytest.raises(ValueError):
|
|
91
|
+
f()
|
|
92
|
+
assert order == ["before", "after", "before", "after"]
|
|
93
|
+
|
|
94
|
+
def test_before_and_after_multiple_attempts_success(self):
|
|
95
|
+
calls = []
|
|
96
|
+
order = []
|
|
97
|
+
|
|
98
|
+
def before(details):
|
|
99
|
+
order.append("before")
|
|
100
|
+
|
|
101
|
+
def after(details):
|
|
102
|
+
order.append("after")
|
|
103
|
+
|
|
104
|
+
@backon.on_exception(
|
|
105
|
+
backon.constant,
|
|
106
|
+
ValueError,
|
|
107
|
+
max_tries=3,
|
|
108
|
+
jitter=None,
|
|
109
|
+
interval=0.01,
|
|
110
|
+
before=before,
|
|
111
|
+
after=after,
|
|
112
|
+
)
|
|
113
|
+
def f():
|
|
114
|
+
calls.append(1)
|
|
115
|
+
if len(calls) < 3:
|
|
116
|
+
raise ValueError("fail")
|
|
117
|
+
return "ok"
|
|
118
|
+
|
|
119
|
+
f()
|
|
120
|
+
assert order == ["before", "after", "before", "after", "before", "after"]
|
|
121
|
+
assert len(calls) == 3
|
|
122
|
+
|
|
123
|
+
def test_before_after_via_retry_functional_api(self):
|
|
124
|
+
order = []
|
|
125
|
+
calls = []
|
|
126
|
+
|
|
127
|
+
def before(details):
|
|
128
|
+
order.append("before")
|
|
129
|
+
|
|
130
|
+
def after(details):
|
|
131
|
+
order.append("after")
|
|
132
|
+
|
|
133
|
+
def flaky():
|
|
134
|
+
calls.append(1)
|
|
135
|
+
if len(calls) < 2:
|
|
136
|
+
raise ValueError("fail")
|
|
137
|
+
return "ok"
|
|
138
|
+
|
|
139
|
+
result = backon.retry(
|
|
140
|
+
flaky,
|
|
141
|
+
backon.constant,
|
|
142
|
+
exception=ValueError,
|
|
143
|
+
max_tries=3,
|
|
144
|
+
jitter=None,
|
|
145
|
+
interval=0.01,
|
|
146
|
+
before=before,
|
|
147
|
+
after=after,
|
|
148
|
+
)
|
|
149
|
+
assert result == "ok"
|
|
150
|
+
assert order == ["before", "after", "before", "after"]
|
|
151
|
+
|
|
152
|
+
def test_before_after_multiple_handlers(self):
|
|
153
|
+
order = []
|
|
154
|
+
|
|
155
|
+
def h1(d):
|
|
156
|
+
order.append("h1")
|
|
157
|
+
|
|
158
|
+
def h2(d):
|
|
159
|
+
order.append("h2")
|
|
160
|
+
|
|
161
|
+
@backon.on_exception(
|
|
162
|
+
backon.constant,
|
|
163
|
+
ValueError,
|
|
164
|
+
max_tries=2,
|
|
165
|
+
jitter=None,
|
|
166
|
+
interval=0.01,
|
|
167
|
+
before=[h1, h2],
|
|
168
|
+
after=[h2, h1],
|
|
169
|
+
)
|
|
170
|
+
def f():
|
|
171
|
+
raise ValueError("fail")
|
|
172
|
+
|
|
173
|
+
with pytest.raises(ValueError):
|
|
174
|
+
f()
|
|
175
|
+
assert order == ["h1", "h2", "h2", "h1", "h1", "h2", "h2", "h1"]
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_before_after_async(self):
|
|
179
|
+
order = []
|
|
180
|
+
|
|
181
|
+
def before(details):
|
|
182
|
+
order.append("before")
|
|
183
|
+
|
|
184
|
+
def after(details):
|
|
185
|
+
order.append("after")
|
|
186
|
+
|
|
187
|
+
@backon.on_exception(
|
|
188
|
+
backon.constant,
|
|
189
|
+
ValueError,
|
|
190
|
+
max_tries=3,
|
|
191
|
+
jitter=None,
|
|
192
|
+
interval=0.01,
|
|
193
|
+
before=before,
|
|
194
|
+
after=after,
|
|
195
|
+
)
|
|
196
|
+
async def f():
|
|
197
|
+
return "ok"
|
|
198
|
+
|
|
199
|
+
await f()
|
|
200
|
+
assert order == ["before", "after"]
|
|
201
|
+
|
|
202
|
+
@pytest.mark.asyncio
|
|
203
|
+
async def test_before_after_async_exception(self):
|
|
204
|
+
order = []
|
|
205
|
+
|
|
206
|
+
def before(details):
|
|
207
|
+
order.append("before")
|
|
208
|
+
|
|
209
|
+
def after(details):
|
|
210
|
+
order.append("after")
|
|
211
|
+
|
|
212
|
+
@backon.on_exception(
|
|
213
|
+
backon.constant,
|
|
214
|
+
ValueError,
|
|
215
|
+
max_tries=2,
|
|
216
|
+
jitter=None,
|
|
217
|
+
interval=0.01,
|
|
218
|
+
before=before,
|
|
219
|
+
after=after,
|
|
220
|
+
raise_on_giveup=True,
|
|
221
|
+
)
|
|
222
|
+
async def f():
|
|
223
|
+
raise ValueError("fail")
|
|
224
|
+
|
|
225
|
+
with pytest.raises(ValueError):
|
|
226
|
+
await f()
|
|
227
|
+
assert order == ["before", "after", "before", "after"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestRetryingStatistics:
|
|
231
|
+
def test_statistics_available_after_call(self):
|
|
232
|
+
r = backon.Retrying(
|
|
233
|
+
backon.constant,
|
|
234
|
+
exception=ValueError,
|
|
235
|
+
max_tries=2,
|
|
236
|
+
jitter=None,
|
|
237
|
+
interval=0.01,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def flaky():
|
|
241
|
+
raise ValueError("fail")
|
|
242
|
+
|
|
243
|
+
with pytest.raises(ValueError):
|
|
244
|
+
r.call(flaky)
|
|
245
|
+
|
|
246
|
+
stats = r.statistics
|
|
247
|
+
assert "attempt_number" in stats
|
|
248
|
+
assert stats["attempt_number"] == 2
|
|
249
|
+
|
|
250
|
+
def test_statistics_before_call(self):
|
|
251
|
+
r = backon.Retrying(backon.constant)
|
|
252
|
+
assert r.statistics == {}
|
|
253
|
+
|
|
254
|
+
def test_call_state_after_call(self):
|
|
255
|
+
r = backon.Retrying(
|
|
256
|
+
backon.constant,
|
|
257
|
+
exception=ValueError,
|
|
258
|
+
max_tries=2,
|
|
259
|
+
jitter=None,
|
|
260
|
+
interval=0.01,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def flaky():
|
|
264
|
+
raise ValueError("fail")
|
|
265
|
+
|
|
266
|
+
with pytest.raises(ValueError):
|
|
267
|
+
r.call(flaky)
|
|
268
|
+
|
|
269
|
+
cs = r.call_state
|
|
270
|
+
assert cs is not None
|
|
271
|
+
assert cs.attempt_number == 2
|
|
272
|
+
|
|
273
|
+
def test_call_state_before_call(self):
|
|
274
|
+
r = backon.Retrying(backon.constant)
|
|
275
|
+
assert r.call_state is None
|
|
276
|
+
|
|
277
|
+
def test_on_predicate_statistics(self):
|
|
278
|
+
r = backon.Retrying(
|
|
279
|
+
backon.constant,
|
|
280
|
+
predicate=lambda x: x is None,
|
|
281
|
+
max_tries=3,
|
|
282
|
+
jitter=None,
|
|
283
|
+
interval=0.01,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def flaky():
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
r.call(flaky)
|
|
290
|
+
stats = r.statistics
|
|
291
|
+
assert "attempt_number" in stats
|
|
292
|
+
assert stats["attempt_number"] == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|