pydocket 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
Potentially problematic release.
This version of pydocket might be problematic. Click here for more details.
- docket/__init__.py +2 -0
- docket/dependencies.py +28 -0
- docket/execution.py +16 -1
- docket/instrumentation.py +6 -0
- docket/worker.py +55 -15
- {pydocket-0.2.1.dist-info → pydocket-0.3.1.dist-info}/METADATA +1 -1
- pydocket-0.3.1.dist-info/RECORD +16 -0
- pydocket-0.2.1.dist-info/RECORD +0 -16
- {pydocket-0.2.1.dist-info → pydocket-0.3.1.dist-info}/WHEEL +0 -0
- {pydocket-0.2.1.dist-info → pydocket-0.3.1.dist-info}/entry_points.txt +0 -0
- {pydocket-0.2.1.dist-info → pydocket-0.3.1.dist-info}/licenses/LICENSE +0 -0
docket/__init__.py
CHANGED
|
@@ -14,6 +14,7 @@ from .dependencies import (
|
|
|
14
14
|
CurrentExecution,
|
|
15
15
|
CurrentWorker,
|
|
16
16
|
ExponentialRetry,
|
|
17
|
+
Perpetual,
|
|
17
18
|
Retry,
|
|
18
19
|
TaskKey,
|
|
19
20
|
TaskLogger,
|
|
@@ -34,5 +35,6 @@ __all__ = [
|
|
|
34
35
|
"Retry",
|
|
35
36
|
"ExponentialRetry",
|
|
36
37
|
"Logged",
|
|
38
|
+
"Perpetual",
|
|
37
39
|
"__version__",
|
|
38
40
|
]
|
docket/dependencies.py
CHANGED
|
@@ -126,6 +126,34 @@ class ExponentialRetry(Retry):
|
|
|
126
126
|
return retry
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class Perpetual(Dependency):
|
|
130
|
+
single = True
|
|
131
|
+
|
|
132
|
+
every: timedelta
|
|
133
|
+
args: tuple[Any, ...]
|
|
134
|
+
kwargs: dict[str, Any]
|
|
135
|
+
cancelled: bool
|
|
136
|
+
|
|
137
|
+
def __init__(self, every: timedelta = timedelta(0)) -> None:
|
|
138
|
+
self.every = every
|
|
139
|
+
self.cancelled = False
|
|
140
|
+
|
|
141
|
+
def __call__(
|
|
142
|
+
self, docket: Docket, worker: Worker, execution: Execution
|
|
143
|
+
) -> "Perpetual":
|
|
144
|
+
perpetual = Perpetual(every=self.every)
|
|
145
|
+
perpetual.args = execution.args
|
|
146
|
+
perpetual.kwargs = execution.kwargs
|
|
147
|
+
return perpetual
|
|
148
|
+
|
|
149
|
+
def cancel(self) -> None:
|
|
150
|
+
self.cancelled = True
|
|
151
|
+
|
|
152
|
+
def perpetuate(self, *args: Any, **kwargs: Any) -> None:
|
|
153
|
+
self.args = args
|
|
154
|
+
self.kwargs = kwargs
|
|
155
|
+
|
|
156
|
+
|
|
129
157
|
def get_dependency_parameters(
|
|
130
158
|
function: Callable[..., Awaitable[Any]],
|
|
131
159
|
) -> dict[str, Dependency]:
|
docket/execution.py
CHANGED
|
@@ -183,18 +183,33 @@ TaskStrikes = dict[str, ParameterStrikes]
|
|
|
183
183
|
class StrikeList:
|
|
184
184
|
task_strikes: TaskStrikes
|
|
185
185
|
parameter_strikes: ParameterStrikes
|
|
186
|
+
_conditions: list[Callable[[Execution], bool]]
|
|
186
187
|
|
|
187
188
|
def __init__(self) -> None:
|
|
188
189
|
self.task_strikes = {}
|
|
189
190
|
self.parameter_strikes = {}
|
|
191
|
+
self._conditions = [self._matches_task_or_parameter_strike]
|
|
192
|
+
|
|
193
|
+
def add_condition(self, condition: Callable[[Execution], bool]) -> None:
|
|
194
|
+
"""Adds a temporary condition that indicates an execution is stricken."""
|
|
195
|
+
self._conditions.insert(0, condition)
|
|
196
|
+
|
|
197
|
+
def remove_condition(self, condition: Callable[[Execution], bool]) -> None:
|
|
198
|
+
"""Adds a temporary condition that indicates an execution is stricken."""
|
|
199
|
+
assert condition is not self._matches_task_or_parameter_strike
|
|
200
|
+
self._conditions.remove(condition)
|
|
190
201
|
|
|
191
202
|
def is_stricken(self, execution: Execution) -> bool:
|
|
192
203
|
"""
|
|
193
|
-
Checks if an execution is stricken based on task
|
|
204
|
+
Checks if an execution is stricken based on task, parameter, or temporary
|
|
205
|
+
conditions.
|
|
194
206
|
|
|
195
207
|
Returns:
|
|
196
208
|
bool: True if the execution is stricken, False otherwise.
|
|
197
209
|
"""
|
|
210
|
+
return any(condition(execution) for condition in self._conditions)
|
|
211
|
+
|
|
212
|
+
def _matches_task_or_parameter_strike(self, execution: Execution) -> bool:
|
|
198
213
|
function_name = execution.function.__name__
|
|
199
214
|
|
|
200
215
|
# Check if the entire task is stricken (without parameter conditions)
|
docket/instrumentation.py
CHANGED
|
@@ -70,6 +70,12 @@ TASKS_RETRIED = meter.create_counter(
|
|
|
70
70
|
unit="1",
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
+
TASKS_PERPETUATED = meter.create_counter(
|
|
74
|
+
"docket_tasks_perpetuated",
|
|
75
|
+
description="How many tasks that have been self-perpetuated",
|
|
76
|
+
unit="1",
|
|
77
|
+
)
|
|
78
|
+
|
|
73
79
|
TASK_DURATION = meter.create_histogram(
|
|
74
80
|
"docket_task_duration",
|
|
75
81
|
description="How long tasks take to complete",
|
docket/worker.py
CHANGED
|
@@ -7,7 +7,6 @@ from types import TracebackType
|
|
|
7
7
|
from typing import (
|
|
8
8
|
TYPE_CHECKING,
|
|
9
9
|
Any,
|
|
10
|
-
Callable,
|
|
11
10
|
Mapping,
|
|
12
11
|
Protocol,
|
|
13
12
|
Self,
|
|
@@ -36,6 +35,7 @@ from .instrumentation import (
|
|
|
36
35
|
TASK_PUNCTUALITY,
|
|
37
36
|
TASKS_COMPLETED,
|
|
38
37
|
TASKS_FAILED,
|
|
38
|
+
TASKS_PERPETUATED,
|
|
39
39
|
TASKS_RETRIED,
|
|
40
40
|
TASKS_RUNNING,
|
|
41
41
|
TASKS_STARTED,
|
|
@@ -68,7 +68,6 @@ class Worker:
|
|
|
68
68
|
redelivery_timeout: timedelta
|
|
69
69
|
reconnection_delay: timedelta
|
|
70
70
|
minimum_check_interval: timedelta
|
|
71
|
-
_strike_conditions: list[Callable[[Execution], bool]] = []
|
|
72
71
|
|
|
73
72
|
def __init__(
|
|
74
73
|
self,
|
|
@@ -86,13 +85,9 @@ class Worker:
|
|
|
86
85
|
self.reconnection_delay = reconnection_delay
|
|
87
86
|
self.minimum_check_interval = minimum_check_interval
|
|
88
87
|
|
|
89
|
-
self._strike_conditions = [
|
|
90
|
-
docket.strike_list.is_stricken,
|
|
91
|
-
]
|
|
92
|
-
|
|
93
88
|
async def __aenter__(self) -> Self:
|
|
94
89
|
self._heartbeat_task = asyncio.create_task(self._heartbeat())
|
|
95
|
-
|
|
90
|
+
self._execution_counts = {}
|
|
96
91
|
return self
|
|
97
92
|
|
|
98
93
|
async def __aexit__(
|
|
@@ -101,6 +96,8 @@ class Worker:
|
|
|
101
96
|
exc_value: BaseException | None,
|
|
102
97
|
traceback: TracebackType | None,
|
|
103
98
|
) -> None:
|
|
99
|
+
del self._execution_counts
|
|
100
|
+
|
|
104
101
|
self._heartbeat_task.cancel()
|
|
105
102
|
try:
|
|
106
103
|
await self._heartbeat_task
|
|
@@ -161,6 +158,8 @@ class Worker:
|
|
|
161
158
|
"""Run the worker indefinitely."""
|
|
162
159
|
return await self._run(forever=True) # pragma: no cover
|
|
163
160
|
|
|
161
|
+
_execution_counts: dict[str, int]
|
|
162
|
+
|
|
164
163
|
async def run_at_most(self, iterations_by_key: Mapping[str, int]) -> None:
|
|
165
164
|
"""
|
|
166
165
|
Run the worker until there are no more tasks to process, but limit specified
|
|
@@ -172,23 +171,25 @@ class Worker:
|
|
|
172
171
|
Args:
|
|
173
172
|
iterations_by_key: Maps task keys to their maximum allowed executions
|
|
174
173
|
"""
|
|
175
|
-
|
|
174
|
+
self._execution_counts = {key: 0 for key in iterations_by_key}
|
|
176
175
|
|
|
177
176
|
def has_reached_max_iterations(execution: Execution) -> bool:
|
|
178
|
-
|
|
177
|
+
key = execution.key
|
|
178
|
+
|
|
179
|
+
if key not in iterations_by_key:
|
|
179
180
|
return False
|
|
180
181
|
|
|
181
|
-
if
|
|
182
|
+
if self._execution_counts[key] >= iterations_by_key[key]:
|
|
182
183
|
return True
|
|
183
184
|
|
|
184
|
-
execution_counts[execution.key] += 1
|
|
185
185
|
return False
|
|
186
186
|
|
|
187
|
-
self.
|
|
187
|
+
self.docket.strike_list.add_condition(has_reached_max_iterations)
|
|
188
188
|
try:
|
|
189
189
|
await self.run_until_finished()
|
|
190
190
|
finally:
|
|
191
|
-
self.
|
|
191
|
+
self.docket.strike_list.remove_condition(has_reached_max_iterations)
|
|
192
|
+
self._execution_counts = {}
|
|
192
193
|
|
|
193
194
|
async def _run(self, forever: bool = False) -> None:
|
|
194
195
|
logger.info("Starting worker %r with the following tasks:", self.name)
|
|
@@ -379,12 +380,15 @@ class Worker:
|
|
|
379
380
|
arrow = "↬" if execution.attempt > 1 else "↪"
|
|
380
381
|
call = execution.call_repr()
|
|
381
382
|
|
|
382
|
-
if
|
|
383
|
+
if self.docket.strike_list.is_stricken(execution):
|
|
383
384
|
arrow = "🗙"
|
|
384
385
|
logger.warning("%s %s", arrow, call, extra=log_context)
|
|
385
386
|
TASKS_STRICKEN.add(1, counter_labels | {"docket.where": "worker"})
|
|
386
387
|
return
|
|
387
388
|
|
|
389
|
+
if execution.key in self._execution_counts:
|
|
390
|
+
self._execution_counts[execution.key] += 1
|
|
391
|
+
|
|
388
392
|
dependencies = self._get_dependencies(execution)
|
|
389
393
|
|
|
390
394
|
context = propagate.extract(message, getter=message_getter)
|
|
@@ -424,12 +428,20 @@ class Worker:
|
|
|
424
428
|
TASKS_SUCCEEDED.add(1, counter_labels)
|
|
425
429
|
duration = datetime.now(timezone.utc) - start
|
|
426
430
|
log_context["duration"] = duration.total_seconds()
|
|
427
|
-
|
|
431
|
+
rescheduled = await self._perpetuate_if_requested(
|
|
432
|
+
execution, dependencies, duration
|
|
433
|
+
)
|
|
434
|
+
arrow = "↫" if rescheduled else "↩"
|
|
435
|
+
logger.info("%s [%s] %s", arrow, duration, call, extra=log_context)
|
|
428
436
|
except Exception:
|
|
429
437
|
TASKS_FAILED.add(1, counter_labels)
|
|
430
438
|
duration = datetime.now(timezone.utc) - start
|
|
431
439
|
log_context["duration"] = duration.total_seconds()
|
|
432
440
|
retried = await self._retry_if_requested(execution, dependencies)
|
|
441
|
+
if not retried:
|
|
442
|
+
retried = await self._perpetuate_if_requested(
|
|
443
|
+
execution, dependencies, duration
|
|
444
|
+
)
|
|
433
445
|
arrow = "↫" if retried else "↩"
|
|
434
446
|
logger.exception("%s [%s] %s", arrow, duration, call, extra=log_context)
|
|
435
447
|
finally:
|
|
@@ -481,6 +493,34 @@ class Worker:
|
|
|
481
493
|
|
|
482
494
|
return False
|
|
483
495
|
|
|
496
|
+
async def _perpetuate_if_requested(
|
|
497
|
+
self, execution: Execution, dependencies: dict[str, Any], duration: timedelta
|
|
498
|
+
) -> bool:
|
|
499
|
+
from .dependencies import Perpetual
|
|
500
|
+
|
|
501
|
+
perpetuals = [
|
|
502
|
+
perpetual
|
|
503
|
+
for perpetual in dependencies.values()
|
|
504
|
+
if isinstance(perpetual, Perpetual)
|
|
505
|
+
]
|
|
506
|
+
if not perpetuals:
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
perpetual = perpetuals[0]
|
|
510
|
+
|
|
511
|
+
if perpetual.cancelled:
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
now = datetime.now(timezone.utc)
|
|
515
|
+
execution.when = max(now, now + perpetual.every - duration)
|
|
516
|
+
execution.args = perpetual.args
|
|
517
|
+
execution.kwargs = perpetual.kwargs
|
|
518
|
+
|
|
519
|
+
await self.docket.schedule(execution)
|
|
520
|
+
|
|
521
|
+
TASKS_PERPETUATED.add(1, {**self.labels(), **execution.specific_labels()})
|
|
522
|
+
return True
|
|
523
|
+
|
|
484
524
|
@property
|
|
485
525
|
def workers_set(self) -> str:
|
|
486
526
|
return self.docket.workers_set
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydocket
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A distributed background task system for Python functions
|
|
5
5
|
Project-URL: Homepage, https://github.com/chrisguidry/docket
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
docket/__init__.py,sha256=7oruGALDoU6W_ntF-mMxxv3FFtO970DVzj3lUgoVIiM,775
|
|
2
|
+
docket/__main__.py,sha256=Vkuh7aJ-Bl7QVpVbbkUksAd_hn05FiLmWbc-8kbhZQ4,34
|
|
3
|
+
docket/annotations.py,sha256=GZwOPtPXyeIhnsLh3TQMBnXrjtTtSmF4Ratv4vjPx8U,950
|
|
4
|
+
docket/cli.py,sha256=EseF0Sj7IEgd9QDC-FSbHSffvF7DNsrmDGYGgZBdJc8,19413
|
|
5
|
+
docket/dependencies.py,sha256=S3KqXxEF0Q2t_jO3R-kI5IIA3M-tqybtiSod2xnRO4o,4991
|
|
6
|
+
docket/docket.py,sha256=zva6ofTm7i5hRwAaAnNtlgIqoMPaNLqCTs2PXGka_8s,19723
|
|
7
|
+
docket/execution.py,sha256=PDrlAr8VzmB6JvqKO71YhXUcTcGQW7eyXrSKiTcAexE,12508
|
|
8
|
+
docket/instrumentation.py,sha256=bZlGA02JoJcY0J1WGm5_qXDfY0AXKr0ZLAYu67wkeKY,4611
|
|
9
|
+
docket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
docket/tasks.py,sha256=RIlSM2omh-YDwVnCz6M5MtmK8T_m_s1w2OlRRxDUs6A,1437
|
|
11
|
+
docket/worker.py,sha256=AO5k7rn5xHLGN0L6vckQuMyt_LSHh01me_LqNtsobtc,21786
|
|
12
|
+
pydocket-0.3.1.dist-info/METADATA,sha256=tIHiKQjQ-to6bp437vH3-2LsUPnfUP-iG61FAcQa2GE,13092
|
|
13
|
+
pydocket-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
pydocket-0.3.1.dist-info/entry_points.txt,sha256=4WOk1nUlBsUT5O3RyMci2ImuC5XFswuopElYcLHtD5k,47
|
|
15
|
+
pydocket-0.3.1.dist-info/licenses/LICENSE,sha256=YuVWU_ZXO0K_k2FG8xWKe5RGxV24AhJKTvQmKfqXuyk,1087
|
|
16
|
+
pydocket-0.3.1.dist-info/RECORD,,
|
pydocket-0.2.1.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
docket/__init__.py,sha256=GoJYpyuO6QFeBB8GNaxGGvMMuai55Eaw_8u-o1PM3hk,743
|
|
2
|
-
docket/__main__.py,sha256=Vkuh7aJ-Bl7QVpVbbkUksAd_hn05FiLmWbc-8kbhZQ4,34
|
|
3
|
-
docket/annotations.py,sha256=GZwOPtPXyeIhnsLh3TQMBnXrjtTtSmF4Ratv4vjPx8U,950
|
|
4
|
-
docket/cli.py,sha256=EseF0Sj7IEgd9QDC-FSbHSffvF7DNsrmDGYGgZBdJc8,19413
|
|
5
|
-
docket/dependencies.py,sha256=gIDwcBUhrLk7xGh0ZxdqpsnSeX-hZzGMNvUrVFfqbJI,4281
|
|
6
|
-
docket/docket.py,sha256=zva6ofTm7i5hRwAaAnNtlgIqoMPaNLqCTs2PXGka_8s,19723
|
|
7
|
-
docket/execution.py,sha256=ShP8MoLmxEslk2pAuhKi6KEEKbHdneyQukR9oQwXdjQ,11732
|
|
8
|
-
docket/instrumentation.py,sha256=SUVhVFf8AX2HAfmi0HPTT_QvQezlGPJEKs_1YAmrCbA,4454
|
|
9
|
-
docket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
docket/tasks.py,sha256=RIlSM2omh-YDwVnCz6M5MtmK8T_m_s1w2OlRRxDUs6A,1437
|
|
11
|
-
docket/worker.py,sha256=DH15hW8QBGHaZdOdkpH7bjYtLEydi4sGh-Ei8lEXGOo,20556
|
|
12
|
-
pydocket-0.2.1.dist-info/METADATA,sha256=9DxwXrPzeTCOlxDGn9JUOzQN-k6OjhAJbiRPeMhcNNo,13092
|
|
13
|
-
pydocket-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
pydocket-0.2.1.dist-info/entry_points.txt,sha256=4WOk1nUlBsUT5O3RyMci2ImuC5XFswuopElYcLHtD5k,47
|
|
15
|
-
pydocket-0.2.1.dist-info/licenses/LICENSE,sha256=YuVWU_ZXO0K_k2FG8xWKe5RGxV24AhJKTvQmKfqXuyk,1087
|
|
16
|
-
pydocket-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|