fastprocesses 0.19.2__tar.gz → 0.19.4__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.
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/PKG-INFO +2 -2
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/README.md +1 -1
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/pyproject.toml +1 -1
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/api/manager.py +20 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/common.py +1 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/base_process.py +121 -21
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/config.py +4 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/processes/process_registry.py +1 -1
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/worker/celery_app.py +57 -27
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/AUTHORS.md +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/__init__.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/api/__init__.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/api/router.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/api/server.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/__init__.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/cache.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/exceptions.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/logging.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/models.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/redis_connection.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/core/types.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/processes/__init__.py +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/py.typed +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/static/style.css +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/templates/landing.html +0 -0
- {fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/worker/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fastprocesses
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.4
|
|
4
4
|
Summary: A library to create a FastAPI-based OGC API Processes wrapper around existing projects.
|
|
5
5
|
Author: Stefan Schuhart
|
|
6
6
|
Author-email: stefan.schuhart@gv.hamburg.de
|
|
@@ -38,7 +38,7 @@ A library to create a FastAPI-based OGC API Processes wrapper around existing pr
|
|
|
38
38
|
|
|
39
39
|
AI helped to create this code.
|
|
40
40
|
|
|
41
|
-
## Version: 0.19.
|
|
41
|
+
## Version: 0.19.4
|
|
42
42
|
|
|
43
43
|
### Description
|
|
44
44
|
|
|
@@ -79,7 +79,7 @@ exclude = [
|
|
|
79
79
|
|
|
80
80
|
[tool.poetry]
|
|
81
81
|
name = "fastprocesses"
|
|
82
|
-
version = "0.19.
|
|
82
|
+
version = "0.19.4"
|
|
83
83
|
description = "A library to create a FastAPI-based OGC API Processes wrapper around existing projects."
|
|
84
84
|
authors = ["Stefan Schuhart <stefan.schuhart@gv.hamburg.de>"]
|
|
85
85
|
readme = "README.md"
|
|
@@ -94,6 +94,12 @@ class AsyncExecutionStrategy(ExecutionStrategy):
|
|
|
94
94
|
)
|
|
95
95
|
raise BrokerUnavailableError(str(exc)) from exc
|
|
96
96
|
send_elapsed = time.monotonic() - send_start
|
|
97
|
+
logger.info(
|
|
98
|
+
"Task queued: process_id={}, job_id={}, mode=async, send_time={:.3f}s",
|
|
99
|
+
process_id,
|
|
100
|
+
task.id,
|
|
101
|
+
send_elapsed,
|
|
102
|
+
)
|
|
97
103
|
if send_elapsed > 1.0:
|
|
98
104
|
logger.warning(
|
|
99
105
|
"Celery async send_task for process_id={} took {:.2f}s",
|
|
@@ -155,6 +161,13 @@ class AsyncExecutionStrategy(ExecutionStrategy):
|
|
|
155
161
|
)
|
|
156
162
|
raise BrokerUnavailableError(str(exc)) from exc
|
|
157
163
|
send_elapsed = time.monotonic() - send_start
|
|
164
|
+
logger.info(
|
|
165
|
+
"Task queued: process_id={}, job_id={},"
|
|
166
|
+
" mode=async (raw), send_time={:.3f}s",
|
|
167
|
+
process_id,
|
|
168
|
+
task.id,
|
|
169
|
+
send_elapsed,
|
|
170
|
+
)
|
|
158
171
|
if send_elapsed > 1.0:
|
|
159
172
|
logger.warning(
|
|
160
173
|
"Celery async send_task for process_id={} took {:.2f}s",
|
|
@@ -218,6 +231,13 @@ class SyncExecutionStrategy(ExecutionStrategy):
|
|
|
218
231
|
exc,
|
|
219
232
|
)
|
|
220
233
|
raise BrokerUnavailableError(str(exc)) from exc
|
|
234
|
+
send_elapsed_sync = time.monotonic() - send_start
|
|
235
|
+
logger.info(
|
|
236
|
+
"Task queued: process_id={}, job_id={}, mode=sync, send_time={:.3f}s",
|
|
237
|
+
process_id,
|
|
238
|
+
task.id,
|
|
239
|
+
send_elapsed_sync,
|
|
240
|
+
)
|
|
221
241
|
send_elapsed = time.monotonic() - send_start
|
|
222
242
|
if send_elapsed > 1.0:
|
|
223
243
|
logger.warning(
|
|
@@ -5,6 +5,7 @@ from typing import Any, Awaitable, ClassVar, Dict, List
|
|
|
5
5
|
|
|
6
6
|
from jsonschema import ValidationError as JSONSchemaValidationError
|
|
7
7
|
from jsonschema import validate as jsonschema_validate
|
|
8
|
+
from jsonschema.exceptions import best_match as _jsonschema_best_match
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
from referencing import Registry
|
|
10
11
|
from referencing.exceptions import Unresolvable as _UnresolvableRef
|
|
@@ -13,6 +14,18 @@ from fastprocesses.core.logging import logger
|
|
|
13
14
|
from fastprocesses.core.models import OutputControl, ProcessDescription
|
|
14
15
|
from fastprocesses.core.types import JobProgressCallback
|
|
15
16
|
|
|
17
|
+
# Maps JSON Schema primitive type names to their Python equivalents.
|
|
18
|
+
# Used by quick_validate_inputs for a cheap top-level type check.
|
|
19
|
+
_JS_TYPE_TO_PYTHON: Dict[str, Any] = {
|
|
20
|
+
"string": str,
|
|
21
|
+
"number": (int, float),
|
|
22
|
+
"integer": int,
|
|
23
|
+
"boolean": bool,
|
|
24
|
+
"array": list,
|
|
25
|
+
"object": dict,
|
|
26
|
+
"null": type(None),
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
|
|
17
30
|
class BaseProcess(ABC):
|
|
18
31
|
process_description: ClassVar[ProcessDescription]
|
|
@@ -98,9 +111,10 @@ class BaseProcess(ABC):
|
|
|
98
111
|
unchanged, so existing processes are unaffected.
|
|
99
112
|
|
|
100
113
|
Override this in subclasses whose inputs may be supplied as URIs
|
|
101
|
-
instead of inline data. The worker calls this method *
|
|
102
|
-
``validate_inputs
|
|
103
|
-
|
|
114
|
+
instead of inline data. The worker calls this method *after*
|
|
115
|
+
``validate_inputs`` so that the process description schema is validated
|
|
116
|
+
against the wire-format input (the URI reference object), and *before*
|
|
117
|
+
``late_validate`` which validates the resolved data.
|
|
104
118
|
|
|
105
119
|
The HTTP fetch, authentication, redirect policy, and SSRF checks are
|
|
106
120
|
the responsibility of the overriding application — fastprocesses does
|
|
@@ -128,21 +142,99 @@ class BaseProcess(ABC):
|
|
|
128
142
|
"""
|
|
129
143
|
return exec_body
|
|
130
144
|
|
|
145
|
+
def late_validate(self, inputs: Dict[str, Any]) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Process-specific validation of **resolved** inputs, called after
|
|
148
|
+
:meth:`resolve_remote_inputs` and before ``execute``.
|
|
149
|
+
|
|
150
|
+
The default implementation is a **no-op** that always returns ``True``,
|
|
151
|
+
so existing processes are unaffected.
|
|
152
|
+
|
|
153
|
+
Override this in subclasses that need to validate data that was
|
|
154
|
+
fetched or transformed by ``resolve_remote_inputs`` — for example,
|
|
155
|
+
validating a downloaded GeoJSON FeatureCollection against a
|
|
156
|
+
process-specific Pydantic model, or spot-checking the first feature
|
|
157
|
+
to detect structural problems early.
|
|
158
|
+
|
|
159
|
+
Unlike :meth:`validate_inputs`, which uses the generic process
|
|
160
|
+
description schema, ``late_validate`` has full access to the resolved
|
|
161
|
+
data and can apply arbitrary business logic. Raise ``ValueError`` to
|
|
162
|
+
fail the job with a user-facing message.
|
|
163
|
+
|
|
164
|
+
Example::
|
|
165
|
+
|
|
166
|
+
from mypackage.models import BuildingFeatureCollection
|
|
167
|
+
|
|
168
|
+
def late_validate(self, inputs: dict) -> bool:
|
|
169
|
+
# Validate the fetched collection against a Pydantic model.
|
|
170
|
+
# model_validate raises ValidationError on structural problems.
|
|
171
|
+
BuildingFeatureCollection.model_validate(
|
|
172
|
+
inputs["buildings"]
|
|
173
|
+
)
|
|
174
|
+
return True
|
|
175
|
+
"""
|
|
176
|
+
return True
|
|
177
|
+
|
|
131
178
|
def quick_validate_inputs(self, inputs: Dict[str, Any]) -> bool:
|
|
132
179
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
Cheap structural check run at the API boundary (before the job is queued).
|
|
181
|
+
|
|
182
|
+
Catches the three most common user errors without any recursion:
|
|
183
|
+
1. Unknown field names (e.g. a typo like ``"buldings"`` instead of
|
|
184
|
+
``"buildings"``)
|
|
185
|
+
2. Missing required fields
|
|
186
|
+
3. Top-level type mismatch (e.g. passing a string where an object is
|
|
187
|
+
expected)
|
|
188
|
+
|
|
189
|
+
Deep schema validation (nested properties, ``oneOf``, ``enum``,
|
|
190
|
+
``const``, …) is left to :meth:`validate_inputs` which runs on the
|
|
191
|
+
worker after the job has been accepted.
|
|
135
192
|
"""
|
|
136
193
|
description: ProcessDescription = self.get_description()
|
|
137
|
-
|
|
194
|
+
defined_inputs = description.inputs
|
|
138
195
|
|
|
139
|
-
#
|
|
140
|
-
for
|
|
141
|
-
|
|
196
|
+
# 1. Unknown fields — catches typos before a job is even queued
|
|
197
|
+
unknown = sorted(k for k in inputs if k not in defined_inputs)
|
|
198
|
+
if unknown:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Unknown input(s): {', '.join(unknown)}. "
|
|
201
|
+
f"Expected: {', '.join(sorted(defined_inputs))}."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# 2. Missing required fields
|
|
205
|
+
for name, desc in defined_inputs.items():
|
|
206
|
+
if desc.minOccurs > 0 and name not in inputs:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"Missing required input '{name}'. "
|
|
209
|
+
f"Description: {desc.description}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# 3. Top-level type check — O(n inputs), no recursion into nested data
|
|
213
|
+
for name, value in inputs.items():
|
|
214
|
+
schema_type = defined_inputs[name].scheme.type
|
|
215
|
+
if schema_type is None:
|
|
216
|
+
continue # no top-level type declared (e.g. pure oneOf/allOf)
|
|
217
|
+
declared = [schema_type] if isinstance(schema_type, str) else schema_type
|
|
218
|
+
expected = tuple(
|
|
219
|
+
py_t
|
|
220
|
+
for js_t in declared
|
|
221
|
+
if (py_t := _JS_TYPE_TO_PYTHON.get(js_t)) is not None
|
|
222
|
+
)
|
|
223
|
+
if not expected:
|
|
224
|
+
continue
|
|
225
|
+
# bool is a subclass of int; reject it unless "boolean" is declared
|
|
226
|
+
if isinstance(value, bool) and "boolean" not in declared:
|
|
142
227
|
raise ValueError(
|
|
143
|
-
f"
|
|
144
|
-
f"
|
|
228
|
+
f"Input '{name}' has wrong type: "
|
|
229
|
+
f"expected {', '.join(declared)}, got boolean."
|
|
145
230
|
)
|
|
231
|
+
if not isinstance(value, expected):
|
|
232
|
+
raise ValueError(
|
|
233
|
+
f"Input '{name}' has wrong type: "
|
|
234
|
+
f"expected {', '.join(declared)}, "
|
|
235
|
+
f"got {type(value).__name__}."
|
|
236
|
+
)
|
|
237
|
+
|
|
146
238
|
return True
|
|
147
239
|
|
|
148
240
|
def validate_inputs(self, inputs: Dict[str, Any]) -> bool:
|
|
@@ -163,6 +255,8 @@ class BaseProcess(ABC):
|
|
|
163
255
|
|
|
164
256
|
# First, check all provided inputs
|
|
165
257
|
for input_name, input_value in inputs.items():
|
|
258
|
+
logger.info("Validating input '{}'", input_name)
|
|
259
|
+
|
|
166
260
|
if input_name not in required_inputs:
|
|
167
261
|
raise ValueError(
|
|
168
262
|
f"Provided input '{input_name}' is "
|
|
@@ -179,9 +273,23 @@ class BaseProcess(ABC):
|
|
|
179
273
|
registry=self.schema_registry,
|
|
180
274
|
)
|
|
181
275
|
except JSONSchemaValidationError as e:
|
|
276
|
+
# For oneOf/anyOf/allOf, best_match picks the deepest,
|
|
277
|
+
# most specific sub-error instead of the generic top-level one.
|
|
278
|
+
leaf = _jsonschema_best_match(e.context) if e.context else e
|
|
279
|
+
# Build a readable path: features[42].properties.area_m2
|
|
280
|
+
path_parts: list[str] = []
|
|
281
|
+
for step in leaf.absolute_path:
|
|
282
|
+
if isinstance(step, int):
|
|
283
|
+
if path_parts:
|
|
284
|
+
path_parts[-1] += f"[{step}]"
|
|
285
|
+
else:
|
|
286
|
+
path_parts.append(f"[{step}]")
|
|
287
|
+
else:
|
|
288
|
+
path_parts.append(str(step))
|
|
289
|
+
location = ".".join(path_parts) or "(root)"
|
|
182
290
|
raise ValueError(
|
|
183
|
-
f"Input '{input_name}' validation failed:
|
|
184
|
-
f"
|
|
291
|
+
f"Input '{input_name}' validation failed: "
|
|
292
|
+
f"{leaf.message} (at: {location})."
|
|
185
293
|
)
|
|
186
294
|
except Exception as e:
|
|
187
295
|
# jsonschema wraps referencing.exceptions.Unresolvable as
|
|
@@ -203,14 +311,6 @@ class BaseProcess(ABC):
|
|
|
203
311
|
else:
|
|
204
312
|
raise
|
|
205
313
|
|
|
206
|
-
# Then, check for missing required inputs
|
|
207
|
-
for input_name, input_desc in required_inputs.items():
|
|
208
|
-
if input_desc.minOccurs > 0 and input_name not in inputs:
|
|
209
|
-
raise ValueError(
|
|
210
|
-
f"Missing required input '{input_name}'. "
|
|
211
|
-
f"Description: {input_desc.description}"
|
|
212
|
-
)
|
|
213
|
-
|
|
214
314
|
return True
|
|
215
315
|
|
|
216
316
|
def validate_outputs(
|
|
@@ -74,6 +74,10 @@ class OGCProcessesSettings(BaseSettings):
|
|
|
74
74
|
FP_CELERY_RESULTS_TTL_DAYS: int = 365
|
|
75
75
|
FP_CELERY_TASK_TLIMIT_HARD: int = 900 # seconds
|
|
76
76
|
FP_CELERY_TASK_TLIMIT_SOFT: int = 600 # seconds
|
|
77
|
+
FP_CELERY_QUEUE: str = Field(
|
|
78
|
+
default="celery",
|
|
79
|
+
description="Celery queue name for task routing. Must match the worker -Q flag.",
|
|
80
|
+
)
|
|
77
81
|
FP_CELERY_JOB_MODE: bool = Field(
|
|
78
82
|
default=False,
|
|
79
83
|
description="Enable job mode for graceful shutdown after task completion"
|
{fastprocesses-0.19.2 → fastprocesses-0.19.4}/src/fastprocesses/processes/process_registry.py
RENAMED
|
@@ -15,7 +15,7 @@ class ProcessRegistry:
|
|
|
15
15
|
"""Manages the registration and retrieval of available processs (processes)."""
|
|
16
16
|
|
|
17
17
|
def __init__(self, redis_connection: RedisConnection | None = None):
|
|
18
|
-
self.registry_key = "process_registry"
|
|
18
|
+
self.registry_key = f"process_registry:{settings.FP_CELERY_QUEUE}"
|
|
19
19
|
if redis_connection is None:
|
|
20
20
|
redis_connection = RedisConnection(str(settings.results_cache.connection))
|
|
21
21
|
self.redis_connection = redis_connection
|
|
@@ -3,6 +3,8 @@ import asyncio
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import signal
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
from typing import Any, Dict, cast
|
|
8
10
|
|
|
@@ -581,12 +583,16 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
581
583
|
serialized_data_str = bytes(serialized_data).decode("utf-8")
|
|
582
584
|
data: dict = json.loads(serialized_data_str)
|
|
583
585
|
|
|
584
|
-
logger.info(f"Executing process {process_id} with data {serialized_data[:80]}")
|
|
585
586
|
job_id = self.request.id # Get the task/job ID
|
|
587
|
+
task_start = time.monotonic()
|
|
588
|
+
logger.info(
|
|
589
|
+
"Worker picked up task: process_id={}, job_id={}",
|
|
590
|
+
process_id,
|
|
591
|
+
job_id,
|
|
592
|
+
)
|
|
586
593
|
|
|
587
594
|
# First: Get the process
|
|
588
595
|
try:
|
|
589
|
-
logger.info(f"Worker retrieving process {process_id} from registry")
|
|
590
596
|
process = get_process_registry().get_process(process_id)
|
|
591
597
|
except ValueError as e:
|
|
592
598
|
job_status = JobStatusCode.FAILED
|
|
@@ -600,7 +606,27 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
600
606
|
except ProcessClassNotFoundError as e:
|
|
601
607
|
raise e
|
|
602
608
|
|
|
603
|
-
# Second:
|
|
609
|
+
# Second: validate wire-format inputs against the process description schema
|
|
610
|
+
try:
|
|
611
|
+
update_job_status(
|
|
612
|
+
job_id,
|
|
613
|
+
0,
|
|
614
|
+
"Validating inputs.",
|
|
615
|
+
job_status,
|
|
616
|
+
)
|
|
617
|
+
process.validate_inputs(data["inputs"])
|
|
618
|
+
except ValueError as e:
|
|
619
|
+
logger.error(f"Input validation failed for process {process_id}: {str(e)}")
|
|
620
|
+
job_status = JobStatusCode.FAILED
|
|
621
|
+
update_job_status(
|
|
622
|
+
job_id,
|
|
623
|
+
0,
|
|
624
|
+
str(e),
|
|
625
|
+
job_status,
|
|
626
|
+
)
|
|
627
|
+
raise InputValidationError(process_id, repr(e))
|
|
628
|
+
|
|
629
|
+
# Third: resolve remote inputs (URI strings → downloaded data)
|
|
604
630
|
try:
|
|
605
631
|
update_job_status(
|
|
606
632
|
job_id,
|
|
@@ -617,30 +643,19 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
617
643
|
update_job_status(job_id, 0, str(e), job_status)
|
|
618
644
|
raise InputValidationError(process_id, repr(e))
|
|
619
645
|
|
|
620
|
-
#
|
|
646
|
+
# Fourth: late validation of resolved inputs (process-specific, no-op by default)
|
|
621
647
|
try:
|
|
622
|
-
|
|
623
|
-
update_job_status(
|
|
624
|
-
job_id,
|
|
625
|
-
0,
|
|
626
|
-
"Validating inputs. This may take a while for complex inputs.",
|
|
627
|
-
job_status,
|
|
628
|
-
)
|
|
629
|
-
process.validate_inputs(data["inputs"])
|
|
648
|
+
process.late_validate(data["inputs"])
|
|
630
649
|
except ValueError as e:
|
|
631
|
-
logger.error(
|
|
632
|
-
|
|
633
|
-
update_job_status(
|
|
634
|
-
job_id,
|
|
635
|
-
0,
|
|
636
|
-
str(e),
|
|
637
|
-
job_status,
|
|
650
|
+
logger.error(
|
|
651
|
+
"Late validation failed for process {}: {}", process_id, e
|
|
638
652
|
)
|
|
653
|
+
job_status = JobStatusCode.FAILED
|
|
654
|
+
update_job_status(job_id, 0, str(e), job_status)
|
|
639
655
|
raise InputValidationError(process_id, repr(e))
|
|
640
656
|
|
|
641
657
|
# Fourth: Execute the process
|
|
642
658
|
try:
|
|
643
|
-
logger.info("Worker executing process {}", process_id)
|
|
644
659
|
job_status = JobStatusCode.RUNNING
|
|
645
660
|
# BUG: if redis returns no job_status, this fails and creates a ValueError too
|
|
646
661
|
update_job_status(
|
|
@@ -690,21 +705,33 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
690
705
|
except Exception as e:
|
|
691
706
|
# Update job with error status
|
|
692
707
|
job_status = JobStatusCode.FAILED
|
|
708
|
+
task_elapsed = time.monotonic() - task_start
|
|
693
709
|
|
|
694
710
|
# decide if its a validation error or a general error
|
|
695
711
|
if isinstance(e, ValueError) or isinstance(e, ValidationError):
|
|
696
|
-
logger.error(
|
|
712
|
+
logger.error(
|
|
713
|
+
"Task failed: process_id={}, job_id={}, duration={:.2f}s, reason={}",
|
|
714
|
+
process_id,
|
|
715
|
+
job_id,
|
|
716
|
+
task_elapsed,
|
|
717
|
+
e,
|
|
718
|
+
)
|
|
697
719
|
job_message = e
|
|
698
720
|
raise e
|
|
699
721
|
|
|
700
722
|
# Log the full traceback server-side only — never expose internal
|
|
701
723
|
# class names, source lines, or line numbers to the client.
|
|
702
|
-
logger.
|
|
703
|
-
"
|
|
724
|
+
logger.error(
|
|
725
|
+
"Task failed: process_id={}, job_id={}, duration={:.2f}s, reason={}",
|
|
704
726
|
process_id,
|
|
705
727
|
job_id,
|
|
728
|
+
task_elapsed,
|
|
706
729
|
e,
|
|
707
|
-
|
|
730
|
+
)
|
|
731
|
+
logger.debug(
|
|
732
|
+
"Full traceback for job {}:\n{}",
|
|
733
|
+
job_id,
|
|
734
|
+
traceback.format_exc(),
|
|
708
735
|
)
|
|
709
736
|
|
|
710
737
|
job_message = (
|
|
@@ -720,10 +747,13 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
720
747
|
return None
|
|
721
748
|
|
|
722
749
|
if result:
|
|
723
|
-
|
|
750
|
+
task_elapsed = time.monotonic() - task_start
|
|
724
751
|
logger.info(
|
|
725
|
-
|
|
726
|
-
|
|
752
|
+
"Task completed: process_id={}, job_id={},"
|
|
753
|
+
" duration={:.2f}s",
|
|
754
|
+
process_id,
|
|
755
|
+
job_id,
|
|
756
|
+
task_elapsed,
|
|
727
757
|
)
|
|
728
758
|
job_status = JobStatusCode.SUCCESSFUL
|
|
729
759
|
|
|
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
|