threadmill 0.7.1__tar.gz → 0.7.3__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.1 → threadmill-0.7.3}/PKG-INFO +2 -1
  2. {threadmill-0.7.1 → threadmill-0.7.3}/pyproject.toml +1 -0
  3. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/_version.py +3 -3
  4. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/lua/acquire.lua +9 -4
  5. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/redis.py +4 -0
  6. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/executor.py +17 -5
  7. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/management/commands/threadmill.py +3 -3
  8. {threadmill-0.7.1 → threadmill-0.7.3}/LICENSE +0 -0
  9. {threadmill-0.7.1 → threadmill-0.7.3}/README.md +0 -0
  10. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/__init__.py +0 -0
  11. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/apps.py +0 -0
  12. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/__init__.py +0 -0
  13. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/base.py +0 -0
  14. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/lua/acknowledge.lua +0 -0
  15. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/lua/mover.lua +0 -0
  16. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/backends/lua/reaper.lua +0 -0
  17. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/exceptions.py +0 -0
  18. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/inspector/__init__.py +0 -0
  19. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/inspector/app.py +0 -0
  20. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/inspector/inspector.scss +0 -0
  21. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/inspector/screens.py +0 -0
  22. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/inspector/telemetry.py +0 -0
  23. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/management/__init__.py +0 -0
  24. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/management/commands/__init__.py +0 -0
  25. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/retry.py +0 -0
  26. {threadmill-0.7.1 → threadmill-0.7.3}/threadmill/signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadmill
3
- Version: 0.7.1
3
+ Version: 0.7.3
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>
@@ -18,6 +18,7 @@ Classifier: Operating System :: POSIX
18
18
  Classifier: Programming Language :: Python
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
21
22
  Classifier: Topic :: Communications :: Email
22
23
  Classifier: Topic :: Software Development
23
24
  Classifier: Topic :: Text Processing :: Markup :: Markdown
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python",
25
25
  "Programming Language :: Python :: 3 :: Only",
26
26
  "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
27
28
  "Topic :: Communications :: Email",
28
29
  "Topic :: Software Development",
29
30
  "Topic :: Text Processing :: Markup :: Markdown",
@@ -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.1'
22
- __version_tuple__ = version_tuple = (0, 7, 1)
21
+ __version__ = version = '0.7.3'
22
+ __version_tuple__ = version_tuple = (0, 7, 3)
23
23
 
24
- __commit_id__ = commit_id = 'gc60eae976'
24
+ __commit_id__ = commit_id = 'g0eee0b8b7'
@@ -1,6 +1,7 @@
1
1
  -- Atomically pop the lowest-scored task from any of the given priority queues,
2
2
  -- update its JSON data with worker info, and move it directly to the running
3
- -- set. Iterates queues in key order and returns the first available task.
3
+ -- set. Scans the queues round-robin from ARGV[7] and returns the first available
4
+ -- task, so a backlogged queue cannot starve its neighbours.
4
5
  --
5
6
  -- KEYS[1..N] -- interleaved running keys and queue keys, one pair per queue:
6
7
  -- KEYS[1] = running set, KEYS[2] = queue set, KEYS[3] = running,
@@ -11,12 +12,16 @@
11
12
  -- ARGV[4] -- number of queue pairs (N/2)
12
13
  -- ARGV[5] -- worker name
13
14
  -- ARGV[6] -- lease TTL in milliseconds
15
+ -- ARGV[7] -- start_index; 0-based index of the queue pair to scan first, so
16
+ -- start_index 0 is the pair at KEYS[1] and KEYS[2]
14
17
  -- Returns: updated serialized data on success, nil if all queues are empty.
15
18
 
16
19
  local num_queues = tonumber(ARGV[4])
17
20
  local lease_ttl_ms = tonumber(ARGV[6])
18
- for i = 1, num_queues do
19
- local result = redis.call('ZPOPMIN', KEYS[i * 2])
21
+ local start_index = tonumber(ARGV[7])
22
+ for offset = 0, num_queues - 1 do
23
+ local queue_index = (start_index + offset) % num_queues + 1
24
+ local result = redis.call('ZPOPMIN', KEYS[queue_index * 2])
20
25
  if #result > 0 then
21
26
  local task_id = result[1]
22
27
  local data = redis.call('HGET', ARGV[3] .. task_id, 'data')
@@ -34,7 +39,7 @@ for i = 1, num_queues do
34
39
  table.insert(parsed.worker_ids, ARGV[5])
35
40
  local updated_data = cjson.encode(parsed)
36
41
  local deadline = tonumber(ARGV[1]) + lease_ttl_ms
37
- redis.call('ZADD', KEYS[i * 2 - 1], deadline, task_id)
42
+ redis.call('ZADD', KEYS[queue_index * 2 - 1], deadline, task_id)
38
43
  redis.call('HSET', ARGV[3] .. task_id, 'data', updated_data)
39
44
  return updated_data
40
45
  end
@@ -5,6 +5,7 @@ import dataclasses
5
5
  import datetime
6
6
  import logging
7
7
  import queue
8
+ import random
8
9
  import time
9
10
  import uuid
10
11
  from collections.abc import Generator, Sequence
@@ -162,6 +163,7 @@ class RedisTaskBackend(ThreadmillTaskBackend):
162
163
  "poll_max_interval", datetime.timedelta(seconds=1)
163
164
  )
164
165
  self._miss_count = 0
166
+ self._rotation_offset = random.randrange(len(self.queues)) # noqa: S311
165
167
  self._acquire_script = self.client.register_script(self.ACQUIRE_SCRIPT)
166
168
  self._acknowledge_script = self.client.register_script(self.ACKNOWLEDGE_SCRIPT)
167
169
 
@@ -275,9 +277,11 @@ class RedisTaskBackend(ThreadmillTaskBackend):
275
277
  str(len(queue_names)),
276
278
  worker,
277
279
  str(int(self.lease_ttl.total_seconds() * 1000)),
280
+ str(self._rotation_offset),
278
281
  ],
279
282
  ):
280
283
  self._miss_count = 0
284
+ self._rotation_offset = (self._rotation_offset + 1) % len(queue_names)
281
285
  return self.deserialize_task_result(data)
282
286
 
283
287
  try:
@@ -8,6 +8,7 @@ import logging
8
8
  import multiprocessing
9
9
  import random
10
10
  import socket
11
+ import sys
11
12
  import threading
12
13
  import time
13
14
  import typing
@@ -81,10 +82,21 @@ class JsonFormatter(logging.Formatter):
81
82
 
82
83
 
83
84
  logger = multiprocessing.get_logger()
84
- handler = logging.StreamHandler()
85
+ handler = logging.StreamHandler(sys.stdout)
85
86
  handler.setFormatter(JsonFormatter())
86
- logger.addHandler(handler)
87
- logger.setLevel(logging.INFO)
87
+
88
+
89
+ def configure_logging(formatter: logging.Formatter) -> None:
90
+ """Route every log record of this process through the threadmill handler."""
91
+ handler.setFormatter(formatter)
92
+ for existing_logger in logging.root.manager.loggerDict.values():
93
+ if isinstance(existing_logger, logging.Logger):
94
+ existing_logger.handlers.clear()
95
+ existing_logger.propagate = True
96
+ root_logger = logging.getLogger()
97
+ root_logger.handlers.clear()
98
+ root_logger.addHandler(handler)
99
+ root_logger.setLevel(logging.INFO)
88
100
 
89
101
 
90
102
  @dataclasses.dataclass(kw_only=True, slots=True)
@@ -138,7 +150,7 @@ class TaskExecutor:
138
150
 
139
151
  def run(self) -> None:
140
152
  """Start consuming tasks until shutdown is requested."""
141
- handler.setFormatter(self.log_formatter)
153
+ configure_logging(self.log_formatter)
142
154
  self.worker_processes = [
143
155
  self.create_worker_process() for _ in range(self.process_count)
144
156
  ]
@@ -213,8 +225,8 @@ class WorkerProcess(multiprocessing.Process):
213
225
 
214
226
  def run(self) -> None:
215
227
  """Start consumer execution inside this process."""
216
- handler.setFormatter(self.log_formatter)
217
228
  django.setup()
229
+ configure_logging(self.log_formatter)
218
230
  logger.info("Starting worker process %s", self.name)
219
231
  self.lock = threading.Lock()
220
232
  self.expired = threading.Event()
@@ -38,7 +38,7 @@ class WorkerCommand(DjangoBaseCommand):
38
38
  "--queues",
39
39
  nargs="+",
40
40
  default=[DEFAULT_TASK_QUEUE_NAME],
41
- help="Queue names to listen to and process tasks from.",
41
+ help="Queue names to listen to and process tasks from (round-robin).",
42
42
  )
43
43
  parser.add_argument(
44
44
  "-w",
@@ -88,8 +88,8 @@ class WorkerCommand(DjangoBaseCommand):
88
88
  parser.add_argument(
89
89
  "--log-format",
90
90
  help=(
91
- "Logging format string for worker log records, e.g."
92
- " '%%(levelname)s %%(message)s'. Defaults to JSON."
91
+ "Logging format string for all log records of the worker process,"
92
+ " e.g. '%%(levelname)s %%(message)s'. Defaults to JSON."
93
93
  ),
94
94
  )
95
95
 
File without changes
File without changes