threadmill 0.6.2__tar.gz → 0.7.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.
Files changed (26) hide show
  1. {threadmill-0.6.2 → threadmill-0.7.0}/PKG-INFO +1 -1
  2. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/_version.py +3 -3
  3. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/executor.py +88 -13
  4. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/commands/threadmill.py +22 -1
  5. {threadmill-0.6.2 → threadmill-0.7.0}/LICENSE +0 -0
  6. {threadmill-0.6.2 → threadmill-0.7.0}/README.md +0 -0
  7. {threadmill-0.6.2 → threadmill-0.7.0}/pyproject.toml +0 -0
  8. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/__init__.py +0 -0
  9. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/apps.py +0 -0
  10. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/__init__.py +0 -0
  11. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/base.py +0 -0
  12. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/acknowledge.lua +0 -0
  13. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/acquire.lua +0 -0
  14. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/mover.lua +0 -0
  15. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/reaper.lua +0 -0
  16. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/redis.py +0 -0
  17. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/exceptions.py +0 -0
  18. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/__init__.py +0 -0
  19. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/app.py +0 -0
  20. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/inspector.scss +0 -0
  21. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/screens.py +0 -0
  22. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/telemetry.py +0 -0
  23. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/__init__.py +0 -0
  24. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/commands/__init__.py +0 -0
  25. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/retry.py +0 -0
  26. {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadmill
3
- Version: 0.6.2
3
+ Version: 0.7.0
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,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.6.2'
22
- __version_tuple__ = version_tuple = (0, 6, 2)
21
+ __version__ = version = '0.7.0'
22
+ __version_tuple__ = version_tuple = (0, 7, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g9ded4014b'
24
+ __commit_id__ = commit_id = 'g0fc4d7ade'
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
  import dataclasses
5
5
  import datetime
6
+ import json
6
7
  import logging
7
8
  import multiprocessing
8
9
  import random
@@ -16,6 +17,7 @@ from queue import Empty
16
17
  from traceback import format_exception
17
18
 
18
19
  import django
20
+ from django.core.serializers.json import DjangoJSONEncoder
19
21
  from django.tasks import TaskResult, task_backends
20
22
  from django.tasks.base import TaskContext, TaskError, TaskResultStatus
21
23
  from django.tasks.signals import task_finished, task_started
@@ -25,12 +27,62 @@ from django.utils.json import normalize_json
25
27
  if typing.TYPE_CHECKING:
26
28
  from .backends.base import Broker, ThreadmillTaskBackend
27
29
 
30
+
31
+ class JsonFormatter(logging.Formatter):
32
+ """Format log records as single-line JSON objects."""
33
+
34
+ standard_attributes = frozenset(
35
+ {
36
+ "args",
37
+ "asctime",
38
+ "created",
39
+ "exc_info",
40
+ "exc_text",
41
+ "filename",
42
+ "funcName",
43
+ "levelname",
44
+ "levelno",
45
+ "lineno",
46
+ "message",
47
+ "module",
48
+ "msecs",
49
+ "msg",
50
+ "name",
51
+ "pathname",
52
+ "process",
53
+ "processName",
54
+ "relativeCreated",
55
+ "stack_info",
56
+ "taskName",
57
+ "thread",
58
+ "threadName",
59
+ }
60
+ )
61
+
62
+ def format(self, record: logging.LogRecord) -> str:
63
+ payload = {
64
+ "created_at": datetime.datetime.fromtimestamp(
65
+ record.created, tz=timezone.get_current_timezone()
66
+ ),
67
+ "level": record.levelname,
68
+ "logger": record.name,
69
+ "message": record.getMessage(),
70
+ "process": record.process,
71
+ "process_name": record.processName,
72
+ "thread": record.threadName,
73
+ } | {
74
+ key: value
75
+ for key, value in record.__dict__.items()
76
+ if key not in self.standard_attributes
77
+ }
78
+ if record.exc_info:
79
+ payload["exception"] = "".join(format_exception(*record.exc_info))
80
+ return json.dumps(payload, cls=DjangoJSONEncoder)
81
+
82
+
28
83
  logger = multiprocessing.get_logger()
29
- formatter = logging.Formatter(
30
- "%(levelname)s: %(asctime)s - pid=%(process)s - %(message)s"
31
- )
32
84
  handler = logging.StreamHandler()
33
- handler.setFormatter(formatter)
85
+ handler.setFormatter(JsonFormatter())
34
86
  logger.addHandler(handler)
35
87
  logger.setLevel(logging.INFO)
36
88
 
@@ -53,6 +105,7 @@ class TaskExecutor:
53
105
  queues: tuple[str]
54
106
  broker: Broker | None = dataclasses.field(default=None, init=False)
55
107
  exit_empty: bool = False
108
+ log_formatter: logging.Formatter = dataclasses.field(default_factory=JsonFormatter)
56
109
 
57
110
  def __post_init__(self) -> None:
58
111
  """Initialize derived orchestration fields and queues."""
@@ -69,17 +122,19 @@ class TaskExecutor:
69
122
  def create_worker_process(self) -> WorkerProcess:
70
123
  """Create and start a new worker process."""
71
124
  worker = WorkerProcess(
72
- self.thread_count,
73
- self.get_maximum_tasks_per_child(),
74
- self.backend.alias,
75
- self.queues,
76
- self.exit_empty,
125
+ thread_count=self.thread_count,
126
+ max_tasks=self.get_maximum_tasks_per_child(),
127
+ backend_alias=self.backend.alias,
128
+ queues=self.queues,
129
+ exit_empty=self.exit_empty,
130
+ log_formatter=self.log_formatter,
77
131
  )
78
132
  worker.start()
79
133
  return worker
80
134
 
81
135
  def run(self) -> None:
82
136
  """Start consuming tasks until shutdown is requested."""
137
+ handler.setFormatter(self.log_formatter)
83
138
  self.worker_processes = [
84
139
  self.create_worker_process() for _ in range(self.process_count)
85
140
  ]
@@ -127,11 +182,13 @@ class WorkerProcess(multiprocessing.Process):
127
182
 
128
183
  def __init__(
129
184
  self,
185
+ *,
130
186
  thread_count: int,
131
187
  max_tasks: int | None = None,
132
188
  backend_alias: str = "",
133
189
  queues: tuple[str, ...] = (),
134
190
  exit_empty: bool = False,
191
+ log_formatter: logging.Formatter,
135
192
  ) -> None:
136
193
  """Create process with dedicated thread pool for task execution."""
137
194
  self.shutdown_requested = multiprocessing.Event()
@@ -141,12 +198,14 @@ class WorkerProcess(multiprocessing.Process):
141
198
  self.backend_alias = backend_alias
142
199
  self.queues = queues
143
200
  self.exit_empty = exit_empty
201
+ self.log_formatter = log_formatter
144
202
  self.task_count = 0
145
203
  self.lock: threading.Lock | None = None
146
204
  self.expired: threading.Event | None = None
147
205
 
148
206
  def run(self) -> None:
149
207
  """Start consumer execution inside this process."""
208
+ handler.setFormatter(self.log_formatter)
150
209
  django.setup()
151
210
  logger.info("Starting worker process %s", self.name)
152
211
  self.lock = threading.Lock()
@@ -229,11 +288,19 @@ class WorkerThread(threading.Thread):
229
288
  try:
230
289
  return task_result.task.retry(TaskContext(task_result=task_result))
231
290
  except Exception:
232
- logger.exception("Retry callback failed for task %r", task_result.id)
291
+ logger.exception(
292
+ "Retry callback failed for task '%s@%s'",
293
+ task_result.id,
294
+ task_result.task.module_path,
295
+ )
233
296
 
234
297
  def execute_task_result(self, task_result: TaskResult) -> TaskResult:
235
298
  """Execute task from task result and update result lifecycle state."""
236
- logger.info("Executing task %r", task_result.id)
299
+ logger.info(
300
+ "Executing task '%s@%s'",
301
+ task_result.id,
302
+ task_result.task.module_path,
303
+ )
237
304
  started_at = timezone.now()
238
305
  task_result = dataclasses.replace(
239
306
  task_result,
@@ -252,7 +319,11 @@ class WorkerThread(threading.Thread):
252
319
  errors=[*task_result.errors, WorkerThread.create_task_error(exception)],
253
320
  finished_at=timezone.now(),
254
321
  )
255
- logger.exception("Task failed %r", task_result.id)
322
+ logger.exception(
323
+ "Task '%s@%s' failed",
324
+ task_result.id,
325
+ task_result.task.module_path,
326
+ )
256
327
  else:
257
328
  task_result = dataclasses.replace(
258
329
  task_result,
@@ -262,7 +333,11 @@ class WorkerThread(threading.Thread):
262
333
  object.__setattr__(
263
334
  task_result, "_return_value", normalize_json(return_value)
264
335
  )
265
- logger.info("Task successful %r", task_result.id)
336
+ logger.info(
337
+ "Task '%s@%s' succeeded",
338
+ task_result.id,
339
+ task_result.task.module_path,
340
+ )
266
341
  finally:
267
342
  task_finished.send(TaskExecutor, task_result=task_result)
268
343
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import signal
2
3
  import sys
3
4
 
@@ -10,7 +11,7 @@ from django.tasks import (
10
11
  task_backends,
11
12
  )
12
13
 
13
- from ...executor import TaskExecutor
14
+ from ...executor import JsonFormatter, TaskExecutor
14
15
 
15
16
 
16
17
  def kill_softly(signum, frame):
@@ -71,6 +72,13 @@ class WorkerCommand(DjangoBaseCommand):
71
72
  action="store_true",
72
73
  help="Drain the task queue and exit with 0.",
73
74
  )
75
+ parser.add_argument(
76
+ "--log-format",
77
+ help=(
78
+ "Logging format string for worker log records, e.g."
79
+ " '%%(levelname)s %%(message)s'. Defaults to JSON."
80
+ ),
81
+ )
74
82
 
75
83
  def handle(
76
84
  self,
@@ -83,6 +91,7 @@ class WorkerCommand(DjangoBaseCommand):
83
91
  max_tasks,
84
92
  max_tasks_jitter,
85
93
  exit_empty,
94
+ log_format,
86
95
  **options,
87
96
  ):
88
97
  match sys.platform:
@@ -101,6 +110,17 @@ class WorkerCommand(DjangoBaseCommand):
101
110
  raise CommandError(
102
111
  f"Backend does not support all specified queues: {_non_queues!r}"
103
112
  )
113
+ try:
114
+ log_formatter = (
115
+ JsonFormatter() if log_format is None else logging.Formatter(log_format)
116
+ )
117
+ log_formatter.format(
118
+ logging.LogRecord(
119
+ "threadmill", logging.INFO, __file__, 1, "Ready", (), None
120
+ )
121
+ )
122
+ except (TypeError, ValueError) as e:
123
+ raise CommandError(f"Invalid log format: {log_format!r}") from e
104
124
  exe = TaskExecutor(
105
125
  backend=backend,
106
126
  workers=workers,
@@ -109,6 +129,7 @@ class WorkerCommand(DjangoBaseCommand):
109
129
  max_tasks_jitter=max_tasks_jitter,
110
130
  exit_empty=exit_empty,
111
131
  queues=queues,
132
+ log_formatter=log_formatter,
112
133
  )
113
134
  try:
114
135
  exe.run()
File without changes
File without changes
File without changes