threadmill 0.7.0__tar.gz → 0.7.1__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.
Files changed (26) hide show
  1. {threadmill-0.7.0 → threadmill-0.7.1}/PKG-INFO +9 -7
  2. {threadmill-0.7.0 → threadmill-0.7.1}/README.md +8 -6
  3. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/_version.py +3 -3
  4. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/redis.py +21 -6
  5. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/executor.py +10 -0
  6. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/management/commands/threadmill.py +17 -0
  7. {threadmill-0.7.0 → threadmill-0.7.1}/LICENSE +0 -0
  8. {threadmill-0.7.0 → threadmill-0.7.1}/pyproject.toml +0 -0
  9. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/__init__.py +0 -0
  10. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/apps.py +0 -0
  11. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/__init__.py +0 -0
  12. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/base.py +0 -0
  13. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/lua/acknowledge.lua +0 -0
  14. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/lua/acquire.lua +0 -0
  15. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/lua/mover.lua +0 -0
  16. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/backends/lua/reaper.lua +0 -0
  17. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/exceptions.py +0 -0
  18. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/inspector/__init__.py +0 -0
  19. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/inspector/app.py +0 -0
  20. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/inspector/inspector.scss +0 -0
  21. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/inspector/screens.py +0 -0
  22. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/inspector/telemetry.py +0 -0
  23. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/management/__init__.py +0 -0
  24. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/management/commands/__init__.py +0 -0
  25. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/retry.py +0 -0
  26. {threadmill-0.7.0 → threadmill-0.7.1}/threadmill/signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadmill
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: The most reliable backend for Django's task framework.
5
5
  Keywords: Django,tasks,worker
6
6
  Author-email: Johannes Maron <johannes@maron.family>
@@ -151,12 +151,14 @@ uv run manage.py threadmill inspector
151
151
  The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
152
152
  `TASKS` configuration:
153
153
 
154
- | Option | Default | Description |
155
- | ----------------- | ---------------------- | ------------------------------------------------------------ |
156
- | `lease_ttl` | `timedelta(hours=1)` | Max processing time before a started task is marked FAILED. |
157
- | `result_ttl` | `timedelta(days=1)` | How long task results are retained before automatic removal. |
158
- | `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
159
- | `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
154
+ | Option | Default | Description |
155
+ | ------------------- | ------------------------- | ----------------------------------------------------------------------- |
156
+ | `lease_ttl` | `timedelta(hours=1)` | Max processing time before a started task is marked FAILED. |
157
+ | `result_ttl` | `timedelta(days=1)` | How long task results are retained before automatic removal. |
158
+ | `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
159
+ | `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
160
+ | `poll_interval` | `timedelta(seconds=0.01)` | Base wait between idle acquire attempts, doubled after each empty poll. |
161
+ | `poll_max_interval` | `timedelta(seconds=1)` | Max wait between idle acquire attempts. |
160
162
 
161
163
  A task that is started but never acknowledged (lease expired) is marked FAILED
162
164
  with an `AcknowledgementTimeout` error. Set `lease_ttl` comfortably above your
@@ -114,12 +114,14 @@ uv run manage.py threadmill inspector
114
114
  The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
115
115
  `TASKS` configuration:
116
116
 
117
- | Option | Default | Description |
118
- | ----------------- | ---------------------- | ------------------------------------------------------------ |
119
- | `lease_ttl` | `timedelta(hours=1)` | Max processing time before a started task is marked FAILED. |
120
- | `result_ttl` | `timedelta(days=1)` | How long task results are retained before automatic removal. |
121
- | `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
122
- | `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
117
+ | Option | Default | Description |
118
+ | ------------------- | ------------------------- | ----------------------------------------------------------------------- |
119
+ | `lease_ttl` | `timedelta(hours=1)` | Max processing time before a started task is marked FAILED. |
120
+ | `result_ttl` | `timedelta(days=1)` | How long task results are retained before automatic removal. |
121
+ | `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
122
+ | `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
123
+ | `poll_interval` | `timedelta(seconds=0.01)` | Base wait between idle acquire attempts, doubled after each empty poll. |
124
+ | `poll_max_interval` | `timedelta(seconds=1)` | Max wait between idle acquire attempts. |
123
125
 
124
126
  A task that is started but never acknowledged (lease expired) is marked FAILED
125
127
  with an `AcknowledgementTimeout` error. Set `lease_ttl` comfortably above your
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.0'
22
- __version_tuple__ = version_tuple = (0, 7, 0)
21
+ __version__ = version = '0.7.1'
22
+ __version_tuple__ = version_tuple = (0, 7, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g0fc4d7ade'
24
+ __commit_id__ = commit_id = 'gc60eae976'
@@ -155,6 +155,13 @@ class RedisTaskBackend(ThreadmillTaskBackend):
155
155
  self.lease_ttl = self.options.get("lease_ttl", datetime.timedelta(hours=1))
156
156
  self.result_ttl = self.options.get("result_ttl", datetime.timedelta(days=1))
157
157
  self.batch_size = self.options.get("batch_size", 100)
158
+ self.poll_interval = self.options.get(
159
+ "poll_interval", datetime.timedelta(seconds=0.01)
160
+ )
161
+ self.poll_max_interval = self.options.get(
162
+ "poll_max_interval", datetime.timedelta(seconds=1)
163
+ )
164
+ self._miss_count = 0
158
165
  self._acquire_script = self.client.register_script(self.ACQUIRE_SCRIPT)
159
166
  self._acknowledge_script = self.client.register_script(self.ACKNOWLEDGE_SCRIPT)
160
167
 
@@ -270,17 +277,25 @@ class RedisTaskBackend(ThreadmillTaskBackend):
270
277
  str(int(self.lease_ttl.total_seconds() * 1000)),
271
278
  ],
272
279
  ):
280
+ self._miss_count = 0
273
281
  return self.deserialize_task_result(data)
274
282
 
275
283
  try:
276
- if deadline - time.monotonic() <= 0:
277
- raise TimeoutError(
278
- "No task available within the specified timeout."
279
- )
284
+ remaining = deadline - time.monotonic()
280
285
  except TypeError:
281
286
  raise queue.Empty("No task available.")
282
- else:
283
- time.sleep(0.01)
287
+ if remaining <= 0:
288
+ raise TimeoutError("No task available within the specified timeout.")
289
+ # Stop doubling once the interval reaches poll_max_interval; larger
290
+ # exponents would only overflow the float math.
291
+ cap = int(self.poll_max_interval / self.poll_interval).bit_length()
292
+ interval_secs = min(
293
+ self.poll_interval.total_seconds() * 2 ** min(self._miss_count, cap),
294
+ self.poll_max_interval.total_seconds(),
295
+ remaining,
296
+ )
297
+ self._miss_count += 1
298
+ time.sleep(interval_secs)
284
299
 
285
300
  def acknowledge(self, task_result: TaskResult) -> None:
286
301
  serialized = self.serialize_task_result(task_result)
@@ -96,6 +96,8 @@ class TaskExecutor:
96
96
  threads: int = 1
97
97
  max_tasks: int = 0
98
98
  max_tasks_jitter: int = 0
99
+ poll_interval: datetime.timedelta = datetime.timedelta(seconds=0.01)
100
+ poll_max_interval: datetime.timedelta = datetime.timedelta(seconds=1)
99
101
  is_publishing: bool = dataclasses.field(default=True, init=False)
100
102
  worker_processes: list[WorkerProcess] = dataclasses.field(
101
103
  default_factory=list, init=False
@@ -127,6 +129,8 @@ class TaskExecutor:
127
129
  backend_alias=self.backend.alias,
128
130
  queues=self.queues,
129
131
  exit_empty=self.exit_empty,
132
+ poll_interval=self.poll_interval,
133
+ poll_max_interval=self.poll_max_interval,
130
134
  log_formatter=self.log_formatter,
131
135
  )
132
136
  worker.start()
@@ -188,6 +192,8 @@ class WorkerProcess(multiprocessing.Process):
188
192
  backend_alias: str = "",
189
193
  queues: tuple[str, ...] = (),
190
194
  exit_empty: bool = False,
195
+ poll_interval: datetime.timedelta = datetime.timedelta(seconds=0.01),
196
+ poll_max_interval: datetime.timedelta = datetime.timedelta(seconds=1),
191
197
  log_formatter: logging.Formatter,
192
198
  ) -> None:
193
199
  """Create process with dedicated thread pool for task execution."""
@@ -198,6 +204,8 @@ class WorkerProcess(multiprocessing.Process):
198
204
  self.backend_alias = backend_alias
199
205
  self.queues = queues
200
206
  self.exit_empty = exit_empty
207
+ self.poll_interval = poll_interval
208
+ self.poll_max_interval = poll_max_interval
201
209
  self.log_formatter = log_formatter
202
210
  self.task_count = 0
203
211
  self.lock: threading.Lock | None = None
@@ -211,6 +219,8 @@ class WorkerProcess(multiprocessing.Process):
211
219
  self.lock = threading.Lock()
212
220
  self.expired = threading.Event()
213
221
  backend = task_backends[self.backend_alias]
222
+ backend.poll_interval = self.poll_interval
223
+ backend.poll_max_interval = self.poll_max_interval
214
224
  consumer_threads = [
215
225
  WorkerThread(worker=self, index=index, backend=backend)
216
226
  for index in range(self.thread_count)
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import logging
2
3
  import signal
3
4
  import sys
@@ -67,6 +68,18 @@ class WorkerCommand(DjangoBaseCommand):
67
68
  default=0,
68
69
  help="Maximum random jitter to add to the max-tasks value by randint(0, max_tasks_jitter).",
69
70
  )
71
+ parser.add_argument(
72
+ "--poll-interval",
73
+ type=float,
74
+ default=0.01,
75
+ help="Base wait between idle acquire attempts in seconds.",
76
+ )
77
+ parser.add_argument(
78
+ "--poll-max-interval",
79
+ type=float,
80
+ default=1,
81
+ help="Maximum wait between idle acquire attempts in seconds.",
82
+ )
70
83
  parser.add_argument(
71
84
  "--exit-empty",
72
85
  action="store_true",
@@ -90,6 +103,8 @@ class WorkerCommand(DjangoBaseCommand):
90
103
  threads,
91
104
  max_tasks,
92
105
  max_tasks_jitter,
106
+ poll_interval,
107
+ poll_max_interval,
93
108
  exit_empty,
94
109
  log_format,
95
110
  **options,
@@ -127,6 +142,8 @@ class WorkerCommand(DjangoBaseCommand):
127
142
  threads=threads,
128
143
  max_tasks=max_tasks,
129
144
  max_tasks_jitter=max_tasks_jitter,
145
+ poll_interval=datetime.timedelta(seconds=poll_interval),
146
+ poll_max_interval=datetime.timedelta(seconds=poll_max_interval),
130
147
  exit_empty=exit_empty,
131
148
  queues=queues,
132
149
  log_formatter=log_formatter,
File without changes
File without changes