ls-algorithm-plugin-sdk 0.3.3__tar.gz → 0.3.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.
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/PKG-INFO +1 -1
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/pyproject.toml +1 -1
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli_impl/configure.py +12 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli_impl/serve.py +6 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/deployment.py +7 -0
- ls_algorithm_plugin_sdk-0.3.4/src/algorithm_plugin_sdk/log_manager.py +125 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/service.py +80 -8
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/PKG-INFO +1 -1
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/SOURCES.txt +1 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_configure.py +8 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_deployment.py +4 -0
- ls_algorithm_plugin_sdk-0.3.4/tests/test_serve.py +73 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_service.py +172 -0
- ls_algorithm_plugin_sdk-0.3.3/src/algorithm_plugin_sdk/log_manager.py +0 -76
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/README.md +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/setup.cfg +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli_impl/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli_impl/parsing.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli_impl/run.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/context.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/errors.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/examples/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/examples/example_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/examples/simulated_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/gpu_isolation.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/loader.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/webui/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/webui/app.css +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/webui/app.js +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/webui/index.html +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/webui_app.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/dependency_links.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/entry_points.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/requires.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/ls_algorithm_plugin_sdk.egg-info/top_level.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_gpu_isolation.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_sdk_automation.py +0 -0
|
@@ -62,6 +62,16 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
62
62
|
help="maximum concurrent executions (default: GPU count, or 1)",
|
|
63
63
|
)
|
|
64
64
|
parser.add_argument("--scratch-dir")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--local-scratch",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="override request scratchRoot with <repository>/.scratch",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--local-output",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="override request output paths with <repository>/.output/<job-id>",
|
|
74
|
+
)
|
|
65
75
|
parser.add_argument(
|
|
66
76
|
"--gpu-ids",
|
|
67
77
|
type=gpu_ids,
|
|
@@ -409,6 +419,8 @@ def generate_config(
|
|
|
409
419
|
webui=args.webui,
|
|
410
420
|
max_concurrency=max_concurrency,
|
|
411
421
|
scratch_dir=args.scratch_dir,
|
|
422
|
+
local_scratch=args.local_scratch,
|
|
423
|
+
local_output=args.local_output,
|
|
412
424
|
token=args.service_token,
|
|
413
425
|
registration=registration_from_args(args, repository),
|
|
414
426
|
gpu_ids=args.gpu_ids,
|
|
@@ -81,6 +81,8 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
81
81
|
def execute(args: argparse.Namespace) -> int:
|
|
82
82
|
config: DeploymentConfig | None = None
|
|
83
83
|
service: dict[str, object] | None = None
|
|
84
|
+
local_scratch = False
|
|
85
|
+
local_output = False
|
|
84
86
|
if args.config:
|
|
85
87
|
# Apply the process mask before refreshing/loading the algorithm. A
|
|
86
88
|
# repository module may import CUDA during module import.
|
|
@@ -91,6 +93,8 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
91
93
|
args.port = int(service["port"])
|
|
92
94
|
args.max_concurrent_executions = int(service["maxConcurrency"])
|
|
93
95
|
args.scratch_dir = service.get("scratchDir")
|
|
96
|
+
local_scratch = bool(service.get("localScratch", False))
|
|
97
|
+
local_output = bool(service.get("localOutput", False))
|
|
94
98
|
args.gpu_ids = list(service.get("gpuIds", []))
|
|
95
99
|
args.webui = bool(service.get("webui"))
|
|
96
100
|
values = config.process_environment()
|
|
@@ -120,6 +124,8 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
120
124
|
),
|
|
121
125
|
max_concurrent_executions=args.max_concurrent_executions,
|
|
122
126
|
scratch_dir=args.scratch_dir,
|
|
127
|
+
local_scratch=local_scratch,
|
|
128
|
+
local_output=local_output,
|
|
123
129
|
gpu_ids=args.gpu_ids,
|
|
124
130
|
runner_factory=create_runner,
|
|
125
131
|
max_attempts=args.max_attempts,
|
|
@@ -146,6 +146,8 @@ class DeploymentConfig:
|
|
|
146
146
|
gpu_ids: list[int] | None = None,
|
|
147
147
|
environment: dict[str, str] | None = None,
|
|
148
148
|
default_parameters: dict[str, Any] | None = None,
|
|
149
|
+
local_scratch: bool = False,
|
|
150
|
+
local_output: bool = False,
|
|
149
151
|
) -> "DeploymentConfig":
|
|
150
152
|
repository = repository_root(root)
|
|
151
153
|
_make_repository_importable(repository)
|
|
@@ -189,6 +191,8 @@ class DeploymentConfig:
|
|
|
189
191
|
"webui": webui,
|
|
190
192
|
"maxConcurrency": max_concurrency,
|
|
191
193
|
"scratchDir": _absolute(scratch_dir, repository),
|
|
194
|
+
"localScratch": local_scratch,
|
|
195
|
+
"localOutput": local_output,
|
|
192
196
|
"gpuIds": configured_gpu_ids,
|
|
193
197
|
"token": token or None,
|
|
194
198
|
"defaultParameters": dict(default_parameters or {}),
|
|
@@ -268,6 +272,9 @@ class DeploymentConfig:
|
|
|
268
272
|
raise RuntimeError("service port must be in [1, 65535]")
|
|
269
273
|
if concurrency < 1:
|
|
270
274
|
raise RuntimeError("service maxConcurrency must be positive")
|
|
275
|
+
for name in ("localScratch", "localOutput"):
|
|
276
|
+
if not isinstance(service.get(name, False), bool):
|
|
277
|
+
raise RuntimeError(f"service {name} must be a boolean")
|
|
271
278
|
gpu_ids = service.get("gpuIds")
|
|
272
279
|
if not isinstance(gpu_ids, list) or any(
|
|
273
280
|
isinstance(gpu_id, bool) or not isinstance(gpu_id, int) or gpu_id < 0
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .context import utc_now
|
|
11
|
+
|
|
12
|
+
_SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]+")
|
|
13
|
+
_TIMESTAMP_GLOB = "[0-9]" * 14
|
|
14
|
+
_FILE_LOCKS_GUARD = threading.Lock()
|
|
15
|
+
_FILE_LOCKS: dict[Path, threading.RLock] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _safe_name(value: str) -> str:
|
|
19
|
+
cleaned = _SAFE_NAME.sub("_", str(value).strip()).strip("._")
|
|
20
|
+
return cleaned or "job"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _file_lock(path: Path) -> threading.RLock:
|
|
24
|
+
with _FILE_LOCKS_GUARD:
|
|
25
|
+
return _FILE_LOCKS.setdefault(path, threading.RLock())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _timestamp_prefix() -> str:
|
|
29
|
+
return utc_now().replace("-", "").replace(":", "").replace("T", "")[:14]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _job_log_path(directory: Path, job_id: str) -> Path:
|
|
33
|
+
safe_job_id = _safe_name(job_id)
|
|
34
|
+
pattern = f"{_TIMESTAMP_GLOB}-{safe_job_id}.log"
|
|
35
|
+
with _FILE_LOCKS_GUARD:
|
|
36
|
+
existing = sorted(directory.glob(pattern))
|
|
37
|
+
if existing:
|
|
38
|
+
return existing[0]
|
|
39
|
+
timestamp = _timestamp_prefix()
|
|
40
|
+
path = directory / f"{timestamp}-{safe_job_id}.log"
|
|
41
|
+
path.touch(exist_ok=True)
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExecutionLog:
|
|
46
|
+
"""Per-job, line-buffered log sink attached to context.logger."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
log_dir: str | Path,
|
|
52
|
+
job_id: str,
|
|
53
|
+
execution_id: str,
|
|
54
|
+
logger: logging.Logger,
|
|
55
|
+
task_id: str | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
resolved_task_id = task_id or execution_id
|
|
58
|
+
self._event_context = {
|
|
59
|
+
"jobId": job_id,
|
|
60
|
+
"taskId": resolved_task_id,
|
|
61
|
+
"executionId": execution_id,
|
|
62
|
+
"job_id": job_id,
|
|
63
|
+
"task_id": resolved_task_id,
|
|
64
|
+
"execution_id": execution_id,
|
|
65
|
+
}
|
|
66
|
+
directory = Path(log_dir).expanduser()
|
|
67
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
path = _job_log_path(directory, job_id)
|
|
69
|
+
self.path = path
|
|
70
|
+
self._stream = path.open("a", encoding="utf-8", buffering=1)
|
|
71
|
+
self._lock = _file_lock(path)
|
|
72
|
+
self._handler = _FlushFileHandler(self._stream, self._lock)
|
|
73
|
+
self._handler.setFormatter(
|
|
74
|
+
logging.Formatter(
|
|
75
|
+
f"%(asctime)s %(levelname)s [executionId={execution_id}] %(message)s"
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
self._handler.setLevel(logging.DEBUG)
|
|
79
|
+
self.logger = logger
|
|
80
|
+
self.logger.setLevel(logging.DEBUG)
|
|
81
|
+
self.logger.addHandler(self._handler)
|
|
82
|
+
self._closed = False
|
|
83
|
+
|
|
84
|
+
def write_event(self, event: str, **values: Any) -> None:
|
|
85
|
+
payload = {
|
|
86
|
+
"event": event,
|
|
87
|
+
"timestamp": utc_now(),
|
|
88
|
+
**self._event_context,
|
|
89
|
+
**values,
|
|
90
|
+
}
|
|
91
|
+
with self._lock:
|
|
92
|
+
if self._closed:
|
|
93
|
+
return
|
|
94
|
+
self._stream.write(json.dumps(payload, ensure_ascii=False, default=str) + "\n")
|
|
95
|
+
self._stream.flush()
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
with self._lock:
|
|
99
|
+
if self._closed:
|
|
100
|
+
return
|
|
101
|
+
self._closed = True
|
|
102
|
+
self.logger.removeHandler(self._handler)
|
|
103
|
+
self._handler.close()
|
|
104
|
+
self._stream.close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _FlushFileHandler(logging.Handler):
|
|
108
|
+
def __init__(self, stream: Any, lock: threading.RLock) -> None:
|
|
109
|
+
super().__init__()
|
|
110
|
+
self.stream = stream
|
|
111
|
+
self._lock = lock
|
|
112
|
+
|
|
113
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
114
|
+
try:
|
|
115
|
+
with self._lock:
|
|
116
|
+
self.stream.write(self.format(record) + "\n")
|
|
117
|
+
self.stream.flush()
|
|
118
|
+
except Exception:
|
|
119
|
+
self.handleError(record)
|
|
120
|
+
|
|
121
|
+
def close(self) -> None:
|
|
122
|
+
try:
|
|
123
|
+
self.stream.flush()
|
|
124
|
+
finally:
|
|
125
|
+
super().close()
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/service.py
RENAMED
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
import random
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
|
+
import traceback
|
|
9
10
|
import uuid
|
|
10
11
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
11
12
|
from contextlib import asynccontextmanager
|
|
@@ -123,6 +124,8 @@ class ExecutionManager:
|
|
|
123
124
|
scratch_dir: str | None = None,
|
|
124
125
|
repository: str | Path | None = None,
|
|
125
126
|
log_dir: str | Path | None = None,
|
|
127
|
+
local_scratch: bool = False,
|
|
128
|
+
local_output: bool = False,
|
|
126
129
|
gpu_ids: list[int] | None = None,
|
|
127
130
|
runner_factory: RunnerFactory | None = None,
|
|
128
131
|
max_attempts: int = MAX_ATTEMPTS,
|
|
@@ -138,6 +141,17 @@ class ExecutionManager:
|
|
|
138
141
|
raise ValueError("retry_seconds must be positive")
|
|
139
142
|
if not 0 <= retry_jitter_ratio <= 1:
|
|
140
143
|
raise ValueError("retry_jitter_ratio must be in [0, 1]")
|
|
144
|
+
if not isinstance(local_scratch, bool) or not isinstance(
|
|
145
|
+
local_output, bool
|
|
146
|
+
):
|
|
147
|
+
raise ValueError("local_scratch and local_output must be booleans")
|
|
148
|
+
repository_path = (
|
|
149
|
+
Path(repository).expanduser().resolve()
|
|
150
|
+
if repository is not None
|
|
151
|
+
else None
|
|
152
|
+
)
|
|
153
|
+
if (local_scratch or local_output) and repository_path is None:
|
|
154
|
+
raise ValueError("repository is required for local scratch or output")
|
|
141
155
|
configured_gpu_ids = None if gpu_ids is None else list(gpu_ids)
|
|
142
156
|
if configured_gpu_ids is not None and (
|
|
143
157
|
any(
|
|
@@ -156,10 +170,19 @@ class ExecutionManager:
|
|
|
156
170
|
raise ValueError("default_parameters must be a JSON object")
|
|
157
171
|
self.default_parameters = dict(default_parameters or {})
|
|
158
172
|
self.scratch_dir = scratch_dir
|
|
173
|
+
self._local_scratch_root = (
|
|
174
|
+
repository_path / ".scratch" if local_scratch else None
|
|
175
|
+
)
|
|
176
|
+
self._local_output_root = (
|
|
177
|
+
repository_path / ".output" if local_output else None
|
|
178
|
+
)
|
|
179
|
+
for local_root in (self._local_scratch_root, self._local_output_root):
|
|
180
|
+
if local_root is not None:
|
|
181
|
+
local_root.mkdir(parents=True, exist_ok=True)
|
|
159
182
|
self.log_dir = (
|
|
160
183
|
Path(log_dir).expanduser()
|
|
161
184
|
if log_dir is not None
|
|
162
|
-
else (
|
|
185
|
+
else (repository_path / "logs" if repository_path is not None else None)
|
|
163
186
|
)
|
|
164
187
|
self.gpu_ids = configured_gpu_ids
|
|
165
188
|
self.max_concurrent_executions = max_concurrent_executions
|
|
@@ -191,6 +214,46 @@ class ExecutionManager:
|
|
|
191
214
|
self.runner.start()
|
|
192
215
|
self._started = True
|
|
193
216
|
|
|
217
|
+
def _apply_local_paths(
|
|
218
|
+
self,
|
|
219
|
+
request: AlgorithmRequest,
|
|
220
|
+
job_id: str,
|
|
221
|
+
) -> AlgorithmRequest:
|
|
222
|
+
workspace = dict(request.workspace or {})
|
|
223
|
+
inputs = request.inputs
|
|
224
|
+
if self._local_scratch_root is not None:
|
|
225
|
+
workspace["scratchRoot"] = str(self._local_scratch_root)
|
|
226
|
+
if self._local_output_root is not None:
|
|
227
|
+
workspace["outputRoot"] = str(self._local_output_root)
|
|
228
|
+
job_output = self._job_output_dir(job_id)
|
|
229
|
+
if len(inputs) == 1:
|
|
230
|
+
output_paths = [job_output]
|
|
231
|
+
else:
|
|
232
|
+
output_paths = [
|
|
233
|
+
job_output / f"{index}-{Path(item.output).name or 'output'}"
|
|
234
|
+
for index, item in enumerate(inputs)
|
|
235
|
+
]
|
|
236
|
+
for output_path in output_paths:
|
|
237
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
inputs = [
|
|
239
|
+
replace(item, output=str(output_path))
|
|
240
|
+
for item, output_path in zip(inputs, output_paths)
|
|
241
|
+
]
|
|
242
|
+
return replace(request, inputs=inputs, workspace=workspace or None)
|
|
243
|
+
|
|
244
|
+
def _job_output_dir(self, job_id: str) -> Path:
|
|
245
|
+
assert self._local_output_root is not None
|
|
246
|
+
if Path(job_id).name != job_id or job_id in {"", ".", ".."}:
|
|
247
|
+
raise ValueError(f"job ID is unsafe for local output: {job_id!r}")
|
|
248
|
+
output = (self._local_output_root / job_id).resolve()
|
|
249
|
+
if (
|
|
250
|
+
output == self._local_output_root
|
|
251
|
+
or self._local_output_root not in output.parents
|
|
252
|
+
):
|
|
253
|
+
raise ValueError(f"job output escapes local root: {job_id!r}")
|
|
254
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
return output
|
|
256
|
+
|
|
194
257
|
def submit(
|
|
195
258
|
self,
|
|
196
259
|
request: AlgorithmRequest,
|
|
@@ -226,6 +289,7 @@ class ExecutionManager:
|
|
|
226
289
|
execution_id = f"exec-{uuid.uuid4().hex}"
|
|
227
290
|
resolved_job_id = str(job_id or execution_id)
|
|
228
291
|
resolved_task_id = str(task_id or execution_id)
|
|
292
|
+
request = self._apply_local_paths(request, resolved_job_id)
|
|
229
293
|
record = ExecutionRecord(
|
|
230
294
|
execution_id=execution_id,
|
|
231
295
|
request=request,
|
|
@@ -237,10 +301,9 @@ class ExecutionManager:
|
|
|
237
301
|
record.context = ExecutionContext(
|
|
238
302
|
execution_id,
|
|
239
303
|
[item.input_dataset for item in request.inputs],
|
|
240
|
-
scratch_dir=(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
else self.scratch_dir
|
|
304
|
+
scratch_dir=(record.request.workspace or {}).get(
|
|
305
|
+
"scratchRoot",
|
|
306
|
+
self.scratch_dir,
|
|
244
307
|
),
|
|
245
308
|
progress_callback=record.add_progress,
|
|
246
309
|
)
|
|
@@ -248,6 +311,7 @@ class ExecutionManager:
|
|
|
248
311
|
record.log = ExecutionLog(
|
|
249
312
|
log_dir=self.log_dir,
|
|
250
313
|
job_id=record.job_id,
|
|
314
|
+
task_id=record.task_id,
|
|
251
315
|
execution_id=record.execution_id,
|
|
252
316
|
logger=record.context.logger,
|
|
253
317
|
)
|
|
@@ -430,10 +494,16 @@ class ExecutionManager:
|
|
|
430
494
|
record.log.write_event("result", state="cancelled", error=str(exc))
|
|
431
495
|
except Exception as exc:
|
|
432
496
|
error = f"{type(exc).__name__}: {exc}"
|
|
497
|
+
stack_trace = traceback.format_exc()
|
|
433
498
|
record.context.mark_unfinished("failed", error)
|
|
434
499
|
record.set_state("failed", error=error)
|
|
435
500
|
if record.log is not None:
|
|
436
|
-
record.log.write_event(
|
|
501
|
+
record.log.write_event(
|
|
502
|
+
"result",
|
|
503
|
+
state="failed",
|
|
504
|
+
error=error,
|
|
505
|
+
traceback=stack_trace,
|
|
506
|
+
)
|
|
437
507
|
else:
|
|
438
508
|
record.set_state(result.status, result=result)
|
|
439
509
|
if record.log is not None:
|
|
@@ -466,7 +536,7 @@ class ExecutionManager:
|
|
|
466
536
|
self.retry_seconds * (1 - self.retry_jitter_ratio),
|
|
467
537
|
self.retry_seconds * (1 + self.retry_jitter_ratio),
|
|
468
538
|
)
|
|
469
|
-
logger.warning(
|
|
539
|
+
record.context.logger.warning(
|
|
470
540
|
"algorithm execution %s failed on attempt %d/%d; "
|
|
471
541
|
"releasing and restarting algorithm in %.3f seconds: %s: %s",
|
|
472
542
|
record.execution_id,
|
|
@@ -475,16 +545,18 @@ class ExecutionManager:
|
|
|
475
545
|
delay,
|
|
476
546
|
type(exc).__name__,
|
|
477
547
|
exc,
|
|
548
|
+
exc_info=True,
|
|
478
549
|
)
|
|
479
550
|
else:
|
|
480
551
|
delay = 0.0
|
|
481
|
-
logger.error(
|
|
552
|
+
record.context.logger.error(
|
|
482
553
|
"algorithm execution %s exhausted %d attempts; "
|
|
483
554
|
"releasing and restarting algorithm for future requests: %s: %s",
|
|
484
555
|
record.execution_id,
|
|
485
556
|
self.max_attempts,
|
|
486
557
|
type(exc).__name__,
|
|
487
558
|
exc,
|
|
559
|
+
exc_info=True,
|
|
488
560
|
)
|
|
489
561
|
finally:
|
|
490
562
|
self._release_runner()
|
|
@@ -140,6 +140,8 @@ class ConfigureTests(unittest.TestCase):
|
|
|
140
140
|
"--no-webui",
|
|
141
141
|
"--max-concurrency", "3",
|
|
142
142
|
"--scratch-dir", "scratch",
|
|
143
|
+
"--local-scratch",
|
|
144
|
+
"--local-output",
|
|
143
145
|
"--gpu-ids", "2,0",
|
|
144
146
|
"--service-token", "secret",
|
|
145
147
|
"--default-parameters", '{"batchSize": 8, "mode": "fast"}',
|
|
@@ -155,6 +157,8 @@ class ConfigureTests(unittest.TestCase):
|
|
|
155
157
|
self.assertFalse(args.webui)
|
|
156
158
|
self.assertEqual(args.max_concurrency, 3)
|
|
157
159
|
self.assertEqual(args.scratch_dir, "scratch")
|
|
160
|
+
self.assertTrue(args.local_scratch)
|
|
161
|
+
self.assertTrue(args.local_output)
|
|
158
162
|
self.assertEqual(args.gpu_ids, [2, 0])
|
|
159
163
|
self.assertEqual(args.service_token, "secret")
|
|
160
164
|
self.assertEqual(
|
|
@@ -186,6 +190,8 @@ class ConfigureTests(unittest.TestCase):
|
|
|
186
190
|
"--local",
|
|
187
191
|
"--config-output", "algorithm-plugin.json",
|
|
188
192
|
"--gpu-ids", "3,1",
|
|
193
|
+
"--local-scratch",
|
|
194
|
+
"--local-output",
|
|
189
195
|
"--env", "DEMO_MODE=fast",
|
|
190
196
|
]
|
|
191
197
|
)
|
|
@@ -207,6 +213,8 @@ class ConfigureTests(unittest.TestCase):
|
|
|
207
213
|
webui=True,
|
|
208
214
|
max_concurrency=2,
|
|
209
215
|
scratch_dir=None,
|
|
216
|
+
local_scratch=True,
|
|
217
|
+
local_output=True,
|
|
210
218
|
token=None,
|
|
211
219
|
registration=None,
|
|
212
220
|
gpu_ids=[3, 1],
|
|
@@ -66,6 +66,8 @@ class DeploymentConfigTests(unittest.TestCase):
|
|
|
66
66
|
webui=True,
|
|
67
67
|
max_concurrency=1,
|
|
68
68
|
scratch_dir=None,
|
|
69
|
+
local_scratch=True,
|
|
70
|
+
local_output=True,
|
|
69
71
|
token=None,
|
|
70
72
|
registration=None,
|
|
71
73
|
gpu_ids=[3, 1],
|
|
@@ -83,6 +85,8 @@ class DeploymentConfigTests(unittest.TestCase):
|
|
|
83
85
|
{"batchSize": 8, "mode": "fast"},
|
|
84
86
|
)
|
|
85
87
|
self.assertEqual(loaded.payload["service"]["gpuIds"], [3, 1])
|
|
88
|
+
self.assertTrue(loaded.payload["service"]["localScratch"])
|
|
89
|
+
self.assertTrue(loaded.payload["service"]["localOutput"])
|
|
86
90
|
manifest_path = root / "release-manifest.json"
|
|
87
91
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
88
92
|
self.assertEqual(
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from test_runner import RecordingAlgorithm
|
|
7
|
+
|
|
8
|
+
from algorithm_plugin_sdk.cli import build_parser
|
|
9
|
+
from algorithm_plugin_sdk.cli_impl.serve import execute
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ServeTests(unittest.TestCase):
|
|
13
|
+
def test_config_passes_local_storage_settings_to_manager(self) -> None:
|
|
14
|
+
args = build_parser().parse_args(
|
|
15
|
+
["serve", "--config", "/srv/demo/algorithm-plugin.json"]
|
|
16
|
+
)
|
|
17
|
+
config = Mock()
|
|
18
|
+
config.algorithm_reference = "demo:Algorithm"
|
|
19
|
+
config.repository = Path("/srv/demo")
|
|
20
|
+
config.payload = {
|
|
21
|
+
"service": {
|
|
22
|
+
"host": "127.0.0.1",
|
|
23
|
+
"port": 9000,
|
|
24
|
+
"maxConcurrency": 2,
|
|
25
|
+
"scratchDir": None,
|
|
26
|
+
"localScratch": True,
|
|
27
|
+
"localOutput": True,
|
|
28
|
+
"gpuIds": [0],
|
|
29
|
+
"webui": False,
|
|
30
|
+
"defaultParameters": {},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
config.process_environment.return_value = dict(os.environ)
|
|
34
|
+
manager = Mock()
|
|
35
|
+
|
|
36
|
+
with (
|
|
37
|
+
patch(
|
|
38
|
+
"algorithm_plugin_sdk.cli_impl.serve.DeploymentConfig.load",
|
|
39
|
+
return_value=config,
|
|
40
|
+
),
|
|
41
|
+
patch(
|
|
42
|
+
"algorithm_plugin_sdk.cli_impl.serve.apply_gpu_isolation",
|
|
43
|
+
return_value=[0],
|
|
44
|
+
),
|
|
45
|
+
patch(
|
|
46
|
+
"algorithm_plugin_sdk.cli_impl.serve.load_algorithm",
|
|
47
|
+
return_value=RecordingAlgorithm(),
|
|
48
|
+
),
|
|
49
|
+
patch(
|
|
50
|
+
"algorithm_plugin_sdk.cli_impl.serve.ReleaseManifest.discover",
|
|
51
|
+
return_value=None,
|
|
52
|
+
),
|
|
53
|
+
patch(
|
|
54
|
+
"algorithm_plugin_sdk.cli_impl.serve.ExecutionManager",
|
|
55
|
+
return_value=manager,
|
|
56
|
+
) as manager_class,
|
|
57
|
+
patch(
|
|
58
|
+
"algorithm_plugin_sdk.cli_impl.serve.PluginRegistrationAgent.from_env",
|
|
59
|
+
return_value=None,
|
|
60
|
+
),
|
|
61
|
+
patch("algorithm_plugin_sdk.cli_impl.serve.run_server"),
|
|
62
|
+
patch("algorithm_plugin_sdk.cli_impl.serve.os.chdir"),
|
|
63
|
+
):
|
|
64
|
+
self.assertEqual(execute(args), 0)
|
|
65
|
+
|
|
66
|
+
manager_kwargs = manager_class.call_args.kwargs
|
|
67
|
+
self.assertTrue(manager_kwargs["local_scratch"])
|
|
68
|
+
self.assertTrue(manager_kwargs["local_output"])
|
|
69
|
+
self.assertEqual(manager_kwargs["repository"], config.repository)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
unittest.main()
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import time
|
|
2
3
|
import unittest
|
|
3
4
|
from contextlib import redirect_stdout
|
|
4
5
|
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
5
8
|
from unittest.mock import patch
|
|
6
9
|
|
|
7
10
|
from test_runner import RecordingAlgorithm
|
|
@@ -57,6 +60,85 @@ class ExecutionManagerTests(unittest.TestCase):
|
|
|
57
60
|
{"batchSize": 16, "mode": "accurate"},
|
|
58
61
|
)
|
|
59
62
|
|
|
63
|
+
def test_local_storage_overrides_request_paths_per_job(self) -> None:
|
|
64
|
+
with TemporaryDirectory() as repository:
|
|
65
|
+
root = Path(repository)
|
|
66
|
+
algorithm = RecordingAlgorithm()
|
|
67
|
+
manager = ExecutionManager(
|
|
68
|
+
AlgorithmRunner(algorithm),
|
|
69
|
+
repository=root,
|
|
70
|
+
local_scratch=True,
|
|
71
|
+
local_output=True,
|
|
72
|
+
)
|
|
73
|
+
request = AlgorithmRequest(
|
|
74
|
+
inputs=[
|
|
75
|
+
AlgorithmInput(
|
|
76
|
+
input_dataset="dataset-a",
|
|
77
|
+
output="/remote/output/dataset-a",
|
|
78
|
+
)
|
|
79
|
+
],
|
|
80
|
+
workspace={
|
|
81
|
+
"inputRoot": "/remote/input",
|
|
82
|
+
"outputRoot": "/remote/output",
|
|
83
|
+
"scratchRoot": "/remote/scratch",
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
first = manager.submit(request, job_id="job-local")
|
|
88
|
+
manager._record(first["executionId"]).future.result(timeout=2)
|
|
89
|
+
second = manager.submit(request, job_id="job-local")
|
|
90
|
+
manager._record(second["executionId"]).future.result(timeout=2)
|
|
91
|
+
manager.close()
|
|
92
|
+
|
|
93
|
+
scratch_root = root / ".scratch"
|
|
94
|
+
output_root = root / ".output"
|
|
95
|
+
job_output = output_root / "job-local"
|
|
96
|
+
self.assertTrue(scratch_root.is_dir())
|
|
97
|
+
self.assertTrue(job_output.is_dir())
|
|
98
|
+
for actual in algorithm.calls:
|
|
99
|
+
self.assertEqual(actual.workspace["inputRoot"], "/remote/input")
|
|
100
|
+
self.assertEqual(actual.workspace["scratchRoot"], str(scratch_root))
|
|
101
|
+
self.assertEqual(actual.workspace["outputRoot"], str(output_root))
|
|
102
|
+
self.assertEqual(actual.inputs[0].output, str(job_output))
|
|
103
|
+
|
|
104
|
+
def test_local_output_rejects_unsafe_job_id(self) -> None:
|
|
105
|
+
with TemporaryDirectory() as repository:
|
|
106
|
+
manager = ExecutionManager(
|
|
107
|
+
AlgorithmRunner(RecordingAlgorithm()),
|
|
108
|
+
repository=repository,
|
|
109
|
+
local_output=True,
|
|
110
|
+
)
|
|
111
|
+
with self.assertRaisesRegex(ValueError, "unsafe"):
|
|
112
|
+
manager.submit(self._request(), job_id="../outside")
|
|
113
|
+
manager.close()
|
|
114
|
+
|
|
115
|
+
def test_local_output_keeps_multiple_input_paths_distinct(self) -> None:
|
|
116
|
+
with TemporaryDirectory() as repository:
|
|
117
|
+
root = Path(repository)
|
|
118
|
+
algorithm = RecordingAlgorithm()
|
|
119
|
+
manager = ExecutionManager(
|
|
120
|
+
AlgorithmRunner(algorithm),
|
|
121
|
+
repository=root,
|
|
122
|
+
local_output=True,
|
|
123
|
+
)
|
|
124
|
+
request = AlgorithmRequest(
|
|
125
|
+
inputs=[
|
|
126
|
+
AlgorithmInput(input_dataset="dataset-a", output="remote/a"),
|
|
127
|
+
AlgorithmInput(input_dataset="dataset-b", output="remote/b"),
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
accepted = manager.submit(request, job_id="job-local")
|
|
132
|
+
manager._record(accepted["executionId"]).future.result(timeout=2)
|
|
133
|
+
manager.close()
|
|
134
|
+
|
|
135
|
+
job_output = root / ".output" / "job-local"
|
|
136
|
+
self.assertEqual(
|
|
137
|
+
[item.output for item in algorithm.calls[0].inputs],
|
|
138
|
+
[str(job_output / "0-a"), str(job_output / "1-b")],
|
|
139
|
+
)
|
|
140
|
+
self.assertTrue(all(path.is_dir() for path in job_output.iterdir()))
|
|
141
|
+
|
|
60
142
|
def test_runs_the_same_algorithm_contract(self) -> None:
|
|
61
143
|
manager = ExecutionManager(AlgorithmRunner(RecordingAlgorithm()))
|
|
62
144
|
accepted = manager.submit(
|
|
@@ -149,6 +231,96 @@ class ExecutionManagerTests(unittest.TestCase):
|
|
|
149
231
|
manager.close()
|
|
150
232
|
self.assertTrue(all(algorithm.stopped == 1 for algorithm in algorithms))
|
|
151
233
|
|
|
234
|
+
def test_reuses_one_log_file_for_executions_of_same_job(self) -> None:
|
|
235
|
+
with TemporaryDirectory() as log_dir:
|
|
236
|
+
first_manager = ExecutionManager(
|
|
237
|
+
AlgorithmRunner(RecordingAlgorithm()),
|
|
238
|
+
log_dir=log_dir,
|
|
239
|
+
)
|
|
240
|
+
with patch(
|
|
241
|
+
"algorithm_plugin_sdk.log_manager._timestamp_prefix",
|
|
242
|
+
return_value="20260910123456",
|
|
243
|
+
):
|
|
244
|
+
first = first_manager.submit(
|
|
245
|
+
self._request(),
|
|
246
|
+
job_id="job-shared",
|
|
247
|
+
task_id="task-a",
|
|
248
|
+
)
|
|
249
|
+
first_manager._record(first["executionId"]).future.result(timeout=2)
|
|
250
|
+
first_manager.close()
|
|
251
|
+
|
|
252
|
+
second_manager = ExecutionManager(
|
|
253
|
+
AlgorithmRunner(RecordingAlgorithm()),
|
|
254
|
+
log_dir=log_dir,
|
|
255
|
+
)
|
|
256
|
+
with patch(
|
|
257
|
+
"algorithm_plugin_sdk.log_manager._timestamp_prefix",
|
|
258
|
+
return_value="20260911123456",
|
|
259
|
+
):
|
|
260
|
+
second = second_manager.submit(
|
|
261
|
+
self._request(),
|
|
262
|
+
job_id="job-shared",
|
|
263
|
+
task_id="task-b",
|
|
264
|
+
)
|
|
265
|
+
second_manager._record(second["executionId"]).future.result(timeout=2)
|
|
266
|
+
second_manager.close()
|
|
267
|
+
|
|
268
|
+
log_files = list(Path(log_dir).glob("*.log"))
|
|
269
|
+
self.assertEqual(
|
|
270
|
+
[path.name for path in log_files],
|
|
271
|
+
["20260910123456-job-shared.log"],
|
|
272
|
+
)
|
|
273
|
+
events = [
|
|
274
|
+
json.loads(line)
|
|
275
|
+
for line in log_files[0].read_text(encoding="utf-8").splitlines()
|
|
276
|
+
]
|
|
277
|
+
request_events = [event for event in events if event["event"] == "request"]
|
|
278
|
+
self.assertEqual(
|
|
279
|
+
[event["executionId"] for event in request_events],
|
|
280
|
+
[first["executionId"], second["executionId"]],
|
|
281
|
+
)
|
|
282
|
+
self.assertTrue(
|
|
283
|
+
all(event["jobId"] == "job-shared" for event in events)
|
|
284
|
+
)
|
|
285
|
+
self.assertEqual(
|
|
286
|
+
{event["taskId"] for event in events},
|
|
287
|
+
{"task-a", "task-b"},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def test_failed_execution_log_includes_full_traceback(self) -> None:
|
|
291
|
+
def create_runner() -> AlgorithmRunner:
|
|
292
|
+
return AlgorithmRunner(RestartProbeAlgorithm(fail=True))
|
|
293
|
+
|
|
294
|
+
with TemporaryDirectory() as log_dir:
|
|
295
|
+
manager = ExecutionManager(
|
|
296
|
+
create_runner(),
|
|
297
|
+
runner_factory=create_runner,
|
|
298
|
+
max_attempts=1,
|
|
299
|
+
log_dir=log_dir,
|
|
300
|
+
)
|
|
301
|
+
accepted = manager.submit(self._request(), job_id="job-failed")
|
|
302
|
+
manager._record(accepted["executionId"]).future.result(timeout=2)
|
|
303
|
+
manager.close()
|
|
304
|
+
|
|
305
|
+
log_files = list(Path(log_dir).glob("??????????????-job-failed.log"))
|
|
306
|
+
self.assertEqual(len(log_files), 1)
|
|
307
|
+
contents = log_files[0].read_text(encoding="utf-8")
|
|
308
|
+
self.assertIn("Traceback (most recent call last):", contents)
|
|
309
|
+
self.assertIn("RuntimeError: simulated algorithm failure", contents)
|
|
310
|
+
events = [
|
|
311
|
+
json.loads(line)
|
|
312
|
+
for line in contents.splitlines()
|
|
313
|
+
if line.startswith("{")
|
|
314
|
+
]
|
|
315
|
+
result_event = events[-1]
|
|
316
|
+
self.assertEqual(result_event["event"], "result")
|
|
317
|
+
self.assertEqual(result_event["state"], "failed")
|
|
318
|
+
self.assertEqual(result_event["executionId"], accepted["executionId"])
|
|
319
|
+
self.assertIn(
|
|
320
|
+
"RuntimeError: simulated algorithm failure",
|
|
321
|
+
result_event["traceback"],
|
|
322
|
+
)
|
|
323
|
+
|
|
152
324
|
def test_next_request_uses_fresh_algorithm_after_attempts_are_exhausted(self) -> None:
|
|
153
325
|
algorithms = []
|
|
154
326
|
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import re
|
|
6
|
-
import threading
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from .context import utc_now
|
|
11
|
-
|
|
12
|
-
_SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]+")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _safe_name(value: str) -> str:
|
|
16
|
-
cleaned = _SAFE_NAME.sub("_", str(value).strip()).strip("._")
|
|
17
|
-
return cleaned or "job"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ExecutionLog:
|
|
21
|
-
"""Per-execution, line-buffered log sink attached to context.logger."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, *, log_dir: str | Path, job_id: str, execution_id: str, logger: logging.Logger) -> None:
|
|
24
|
-
directory = Path(log_dir).expanduser()
|
|
25
|
-
directory.mkdir(parents=True, exist_ok=True)
|
|
26
|
-
path = directory / f"{_safe_name(job_id)}.log"
|
|
27
|
-
if path.exists():
|
|
28
|
-
path = directory / f"{_safe_name(job_id)}-{_safe_name(execution_id)}.log"
|
|
29
|
-
self.path = path
|
|
30
|
-
self._stream = path.open("a", encoding="utf-8", buffering=1)
|
|
31
|
-
self._lock = threading.RLock()
|
|
32
|
-
self._handler = _FlushFileHandler(self._stream)
|
|
33
|
-
self._handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
|
34
|
-
self._handler.setLevel(logging.DEBUG)
|
|
35
|
-
self.logger = logger
|
|
36
|
-
self.logger.setLevel(logging.DEBUG)
|
|
37
|
-
self.logger.addHandler(self._handler)
|
|
38
|
-
self._closed = False
|
|
39
|
-
|
|
40
|
-
def write_event(self, event: str, **values: Any) -> None:
|
|
41
|
-
payload = {"event": event, "timestamp": utc_now(), **values}
|
|
42
|
-
with self._lock:
|
|
43
|
-
if self._closed:
|
|
44
|
-
return
|
|
45
|
-
self._stream.write(json.dumps(payload, ensure_ascii=False, default=str) + "\n")
|
|
46
|
-
self._stream.flush()
|
|
47
|
-
|
|
48
|
-
def close(self) -> None:
|
|
49
|
-
with self._lock:
|
|
50
|
-
if self._closed:
|
|
51
|
-
return
|
|
52
|
-
self._closed = True
|
|
53
|
-
self.logger.removeHandler(self._handler)
|
|
54
|
-
self._handler.close()
|
|
55
|
-
self._stream.close()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class _FlushFileHandler(logging.Handler):
|
|
59
|
-
def __init__(self, stream: Any) -> None:
|
|
60
|
-
super().__init__()
|
|
61
|
-
self.stream = stream
|
|
62
|
-
self._lock = threading.RLock()
|
|
63
|
-
|
|
64
|
-
def emit(self, record: logging.LogRecord) -> None:
|
|
65
|
-
try:
|
|
66
|
-
with self._lock:
|
|
67
|
-
self.stream.write(self.format(record) + "\n")
|
|
68
|
-
self.stream.flush()
|
|
69
|
-
except Exception:
|
|
70
|
-
self.handleError(record)
|
|
71
|
-
|
|
72
|
-
def close(self) -> None:
|
|
73
|
-
try:
|
|
74
|
-
self.stream.flush()
|
|
75
|
-
finally:
|
|
76
|
-
super().close()
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/context.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/loader.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/release.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/src/algorithm_plugin_sdk/runner.py
RENAMED
|
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
|
{ls_algorithm_plugin_sdk-0.3.3 → ls_algorithm_plugin_sdk-0.3.4}/tests/test_sdk_automation.py
RENAMED
|
File without changes
|