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.
- {threadmill-0.6.2 → threadmill-0.7.0}/PKG-INFO +1 -1
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/_version.py +3 -3
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/executor.py +88 -13
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/commands/threadmill.py +22 -1
- {threadmill-0.6.2 → threadmill-0.7.0}/LICENSE +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/README.md +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/pyproject.toml +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/__init__.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/apps.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/__init__.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/base.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/acknowledge.lua +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/acquire.lua +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/mover.lua +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/lua/reaper.lua +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/backends/redis.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/exceptions.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/__init__.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/app.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/inspector.scss +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/screens.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/inspector/telemetry.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/__init__.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/management/commands/__init__.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/retry.py +0 -0
- {threadmill-0.6.2 → threadmill-0.7.0}/threadmill/signals.py +0 -0
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.7.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|