labtasker-client 2.5.0__tar.gz → 2.6.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.
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/PKG-INFO +1 -1
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/pyproject.toml +1 -1
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/__init__.py +1 -1
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/cli.py +48 -50
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/client.py +87 -49
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/command_worker.py +24 -24
- labtasker_client-2.6.0/src/labtasker/config.py +294 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/execution.py +16 -21
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/journal.py +33 -18
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/local.py +40 -48
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/observations.py +4 -2
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/worker.py +150 -15
- labtasker_client-2.5.0/src/labtasker/config.py +0 -213
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/.gitignore +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/LICENSE +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/__main__.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/api.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/binding.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/command_template.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/errors.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/models.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/paths.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/py.typed +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/tee.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/types.py +0 -0
- {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labtasker-client
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: A small task queue for parallel model inference and evaluation
|
|
5
5
|
Project-URL: Homepage, https://github.com/luocfprime/labtasker
|
|
6
6
|
Project-URL: Repository, https://github.com/luocfprime/labtasker.git
|
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
6
9
|
from typing import Annotated, Any, TypeVar, cast
|
|
7
10
|
|
|
8
11
|
import typer
|
|
@@ -15,8 +18,6 @@ from labtasker.command_template import TemplateSyntaxError
|
|
|
15
18
|
from labtasker.command_worker import run_command_worker
|
|
16
19
|
from labtasker.config import resolve_config
|
|
17
20
|
from labtasker.errors import LabtaskerError
|
|
18
|
-
from labtasker.execution import report_progress as report_current_progress
|
|
19
|
-
from labtasker.execution import report_worker_telemetry as report_current_worker_telemetry
|
|
20
21
|
from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
|
|
21
22
|
from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
|
|
22
23
|
|
|
@@ -59,6 +60,15 @@ app.add_typer(config_app, name="config")
|
|
|
59
60
|
logger = logging.getLogger("labtasker.cli")
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class CLIState:
|
|
65
|
+
labtasker_root: Path | None
|
|
66
|
+
auto_start_local_server: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_CLI_STATE: ContextVar[CLIState | None] = ContextVar("labtasker_cli_state", default=None)
|
|
70
|
+
|
|
71
|
+
|
|
62
72
|
def _version_callback(value: bool) -> None:
|
|
63
73
|
if value:
|
|
64
74
|
typer.echo(f"labtasker-client {__version__}")
|
|
@@ -76,8 +86,20 @@ def main(
|
|
|
76
86
|
help="Show the Client package version and exit.",
|
|
77
87
|
),
|
|
78
88
|
] = False,
|
|
89
|
+
labtasker_root: Annotated[
|
|
90
|
+
Path | None,
|
|
91
|
+
typer.Option(help="Exact configuration, journal, and managed-local root."),
|
|
92
|
+
] = None,
|
|
93
|
+
auto_start_local_server: Annotated[
|
|
94
|
+
bool,
|
|
95
|
+
typer.Option(
|
|
96
|
+
"--auto-start-local-server",
|
|
97
|
+
help="Allow this invocation to start a managed-local daemon.",
|
|
98
|
+
),
|
|
99
|
+
] = False,
|
|
79
100
|
) -> None:
|
|
80
101
|
"""Submit, inspect, and execute Labtasker v2 Tasks."""
|
|
102
|
+
_CLI_STATE.set(CLIState(labtasker_root, auto_start_local_server))
|
|
81
103
|
|
|
82
104
|
|
|
83
105
|
class _SeparatedCommand(TyperCommand):
|
|
@@ -161,6 +183,8 @@ def worker_loop(
|
|
|
161
183
|
max_consecutive_failures=max_consecutive_failures,
|
|
162
184
|
force_stop_timeout=force_stop_timeout,
|
|
163
185
|
metadata=worker_metadata,
|
|
186
|
+
labtasker_root=_cli_state().labtasker_root,
|
|
187
|
+
auto_start_local_server=_cli_state().auto_start_local_server,
|
|
164
188
|
)
|
|
165
189
|
except (TemplateSyntaxError, RequestValidationError) as error:
|
|
166
190
|
raise typer.BadParameter(str(error)) from error
|
|
@@ -177,29 +201,6 @@ def worker_loop(
|
|
|
177
201
|
raise typer.Exit(1) from error
|
|
178
202
|
|
|
179
203
|
|
|
180
|
-
@app.command("progress")
|
|
181
|
-
def progress_report(
|
|
182
|
-
data: Annotated[
|
|
183
|
-
str,
|
|
184
|
-
typer.Option(help="Latest progress as one strict JSON object."),
|
|
185
|
-
],
|
|
186
|
-
) -> None:
|
|
187
|
-
"""Replace the current Task run's progress snapshot.
|
|
188
|
-
|
|
189
|
-
This command is available inside a command launched by ``labtasker loop``.
|
|
190
|
-
It prints whether the best-effort report was accepted; transport failures
|
|
191
|
-
and confirmed revocation return ``reported: false`` without failing the
|
|
192
|
-
command workload.
|
|
193
|
-
"""
|
|
194
|
-
try:
|
|
195
|
-
progress = _json_object(data, option="--data")
|
|
196
|
-
reported = _invoke(lambda: report_current_progress(progress))
|
|
197
|
-
except RuntimeError as error:
|
|
198
|
-
typer.echo(str(error), err=True)
|
|
199
|
-
raise typer.Exit(1) from error
|
|
200
|
-
_write_json({"reported": reported})
|
|
201
|
-
|
|
202
|
-
|
|
203
204
|
@task_app.command("submit")
|
|
204
205
|
def task_submit(
|
|
205
206
|
args: Annotated[
|
|
@@ -472,28 +473,6 @@ def worker_count(
|
|
|
472
473
|
_write_json({"count": result} if isinstance(result, int) else result)
|
|
473
474
|
|
|
474
475
|
|
|
475
|
-
@worker_app.command("telemetry")
|
|
476
|
-
def worker_telemetry_report(
|
|
477
|
-
data: Annotated[
|
|
478
|
-
str,
|
|
479
|
-
typer.Option(help="Latest Worker telemetry as one strict JSON object."),
|
|
480
|
-
],
|
|
481
|
-
) -> None:
|
|
482
|
-
"""Replace telemetry for the current Worker invocation.
|
|
483
|
-
|
|
484
|
-
This command is available inside a command launched by ``labtasker loop``.
|
|
485
|
-
It performs one best-effort synchronous report and prints whether the Server
|
|
486
|
-
accepted it.
|
|
487
|
-
"""
|
|
488
|
-
try:
|
|
489
|
-
telemetry = _json_object(data, option="--data")
|
|
490
|
-
reported = _invoke(lambda: report_current_worker_telemetry(telemetry))
|
|
491
|
-
except RuntimeError as error:
|
|
492
|
-
typer.echo(str(error), err=True)
|
|
493
|
-
raise typer.Exit(1) from error
|
|
494
|
-
_write_json({"reported": reported})
|
|
495
|
-
|
|
496
|
-
|
|
497
476
|
def _count_options(
|
|
498
477
|
group_by: list[str] | None, allowed: set[str], limit: int | None, cursor: str | None
|
|
499
478
|
) -> dict[str, Any]:
|
|
@@ -628,7 +607,10 @@ def queue_delete(
|
|
|
628
607
|
name: Annotated[str, typer.Argument(help="Queue name to delete.")],
|
|
629
608
|
cascade: Annotated[
|
|
630
609
|
bool,
|
|
631
|
-
typer.Option(
|
|
610
|
+
typer.Option(
|
|
611
|
+
"--cascade",
|
|
612
|
+
help="Also permanently delete every non-running Task in the Queue.",
|
|
613
|
+
),
|
|
632
614
|
] = False,
|
|
633
615
|
) -> None:
|
|
634
616
|
"""Permanently delete one Queue.
|
|
@@ -647,14 +629,30 @@ def config_show() -> None:
|
|
|
647
629
|
.labtasker/config.toml, then built-in defaults. The token value is never
|
|
648
630
|
printed.
|
|
649
631
|
"""
|
|
650
|
-
|
|
632
|
+
state = _cli_state()
|
|
633
|
+
_write_json(
|
|
634
|
+
_invoke(
|
|
635
|
+
lambda: resolve_config(
|
|
636
|
+
labtasker_root=state.labtasker_root,
|
|
637
|
+
auto_start_local_server=state.auto_start_local_server,
|
|
638
|
+
).public_dict()
|
|
639
|
+
)
|
|
640
|
+
)
|
|
651
641
|
|
|
652
642
|
|
|
653
643
|
def _with_client(operation: Callable[[Client], T]) -> T:
|
|
654
|
-
|
|
644
|
+
state = _cli_state()
|
|
645
|
+
with Client(
|
|
646
|
+
labtasker_root=state.labtasker_root,
|
|
647
|
+
auto_start_local_server=state.auto_start_local_server,
|
|
648
|
+
) as client:
|
|
655
649
|
return operation(client)
|
|
656
650
|
|
|
657
651
|
|
|
652
|
+
def _cli_state() -> CLIState:
|
|
653
|
+
return _CLI_STATE.get() or CLIState(None, False)
|
|
654
|
+
|
|
655
|
+
|
|
658
656
|
def _invoke(operation: Callable[[], T]) -> T:
|
|
659
657
|
try:
|
|
660
658
|
return operation()
|
|
@@ -13,12 +13,7 @@ from pydantic import TypeAdapter, ValidationError
|
|
|
13
13
|
|
|
14
14
|
from labtasker.config import ResolvedConfig, resolve_config
|
|
15
15
|
from labtasker.errors import APIError, TransportError
|
|
16
|
-
from labtasker.local import
|
|
17
|
-
ensure_local_server,
|
|
18
|
-
local_paths,
|
|
19
|
-
require_local_capabilities,
|
|
20
|
-
socket_transport,
|
|
21
|
-
)
|
|
16
|
+
from labtasker.local import ensure_local_server, socket_transport
|
|
22
17
|
from labtasker.models import (
|
|
23
18
|
BulkUpdateResult,
|
|
24
19
|
ClaimResponse,
|
|
@@ -53,7 +48,7 @@ from labtasker.validation import (
|
|
|
53
48
|
|
|
54
49
|
T = TypeVar("T")
|
|
55
50
|
ModelT = TypeVar("ModelT", bound=ResponseModel)
|
|
56
|
-
REQUEST_TIMEOUT_SECONDS =
|
|
51
|
+
REQUEST_TIMEOUT_SECONDS = 15.0
|
|
57
52
|
MAX_RETRY_ATTEMPTS = 3
|
|
58
53
|
RETRY_BACKOFF_SECONDS = (0.05, 0.1)
|
|
59
54
|
QUEUE_LIST_ADAPTER = TypeAdapter(list[Queue])
|
|
@@ -63,21 +58,36 @@ class Client:
|
|
|
63
58
|
def __init__(
|
|
64
59
|
self,
|
|
65
60
|
url: str | None = None,
|
|
61
|
+
socket: str | Path | None = None,
|
|
62
|
+
labtasker_root: str | Path | None = None,
|
|
63
|
+
auto_start_local_server: bool = False,
|
|
66
64
|
token: str | None = None,
|
|
67
65
|
queue: str | None = None,
|
|
68
66
|
) -> None:
|
|
69
|
-
self._initialize(
|
|
67
|
+
self._initialize(
|
|
68
|
+
resolve_config(
|
|
69
|
+
url=url,
|
|
70
|
+
socket=socket,
|
|
71
|
+
labtasker_root=labtasker_root,
|
|
72
|
+
auto_start_local_server=auto_start_local_server,
|
|
73
|
+
token=token,
|
|
74
|
+
queue=queue,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
70
77
|
|
|
71
78
|
@classmethod
|
|
72
|
-
def
|
|
73
|
-
require_local_capabilities()
|
|
79
|
+
def _from_socket(cls, socket: Path, *, queue: str) -> Client:
|
|
74
80
|
client = cls.__new__(cls)
|
|
75
81
|
client._initialize(
|
|
76
82
|
ResolvedConfig(
|
|
77
83
|
url=None,
|
|
84
|
+
socket=socket,
|
|
85
|
+
managed_local=False,
|
|
86
|
+
labtasker_root=Path("/"),
|
|
78
87
|
queue=validate_identifier(queue, field="queue"),
|
|
79
88
|
token=None,
|
|
80
|
-
|
|
89
|
+
auto_start_local_server=False,
|
|
90
|
+
local=None,
|
|
81
91
|
)
|
|
82
92
|
)
|
|
83
93
|
return client
|
|
@@ -87,22 +97,22 @@ class Client:
|
|
|
87
97
|
headers = {}
|
|
88
98
|
if self._config.token is not None:
|
|
89
99
|
headers["Authorization"] = f"Bearer {self._config.token}"
|
|
90
|
-
if self._config.
|
|
91
|
-
assert self._config.url is not None
|
|
100
|
+
if self._config.url is not None:
|
|
92
101
|
self._http = httpx.Client(
|
|
93
102
|
base_url=f"{self._config.url}/api/v2/",
|
|
94
103
|
headers=headers,
|
|
95
104
|
timeout=REQUEST_TIMEOUT_SECONDS,
|
|
96
105
|
)
|
|
97
106
|
else:
|
|
107
|
+
assert self._config.socket is not None
|
|
98
108
|
self._http = httpx.Client(
|
|
99
109
|
base_url="http://labtasker/api/v2/",
|
|
100
|
-
transport=socket_transport(self._config.
|
|
110
|
+
transport=socket_transport(self._config.socket),
|
|
101
111
|
timeout=REQUEST_TIMEOUT_SECONDS,
|
|
102
112
|
)
|
|
103
113
|
self._closed = False
|
|
104
114
|
self._endpoint_announced = False
|
|
105
|
-
self.
|
|
115
|
+
self._local_ensure_result: tuple[int | None, str | None] | None = None
|
|
106
116
|
self._server_version: str | None = None
|
|
107
117
|
self._warned_server_versions: set[Version] = set()
|
|
108
118
|
|
|
@@ -148,7 +158,7 @@ class Client:
|
|
|
148
158
|
)
|
|
149
159
|
|
|
150
160
|
@property
|
|
151
|
-
def
|
|
161
|
+
def _configuration(self) -> ResolvedConfig:
|
|
152
162
|
return self._config
|
|
153
163
|
|
|
154
164
|
def submit_task(
|
|
@@ -538,7 +548,7 @@ class Client:
|
|
|
538
548
|
path=f"queues/{queue_name}/tasks/claim",
|
|
539
549
|
json={"route": normalized_route, "run_id": normalized_run_id},
|
|
540
550
|
parser=_parse_claim,
|
|
541
|
-
|
|
551
|
+
recover_local_connect=False,
|
|
542
552
|
)
|
|
543
553
|
|
|
544
554
|
def _health(self) -> HealthResponse:
|
|
@@ -561,6 +571,7 @@ class Client:
|
|
|
561
571
|
task_id: str,
|
|
562
572
|
run_id: str,
|
|
563
573
|
queue: str | None = None,
|
|
574
|
+
recover_local_connect: bool = True,
|
|
564
575
|
) -> HeartbeatResponse:
|
|
565
576
|
return self._run_action(
|
|
566
577
|
"heartbeat",
|
|
@@ -569,6 +580,7 @@ class Client:
|
|
|
569
580
|
queue=queue,
|
|
570
581
|
body={},
|
|
571
582
|
parser=lambda response: _parse_model(response, HeartbeatResponse, {200}),
|
|
583
|
+
recover_local_connect=recover_local_connect,
|
|
572
584
|
)
|
|
573
585
|
|
|
574
586
|
def _complete(
|
|
@@ -707,6 +719,7 @@ class Client:
|
|
|
707
719
|
queue: str | None,
|
|
708
720
|
body: dict[str, object],
|
|
709
721
|
parser: Callable[[httpx.Response], T],
|
|
722
|
+
recover_local_connect: bool = True,
|
|
710
723
|
) -> T:
|
|
711
724
|
self._ensure_open()
|
|
712
725
|
queue_name = self._queue(queue)
|
|
@@ -718,6 +731,7 @@ class Client:
|
|
|
718
731
|
path=f"queues/{queue_name}/tasks/{normalized_task_id}/{action}",
|
|
719
732
|
json={"run_id": normalized_run_id, **body},
|
|
720
733
|
parser=parser,
|
|
734
|
+
recover_local_connect=recover_local_connect,
|
|
721
735
|
)
|
|
722
736
|
|
|
723
737
|
def _queue(self, queue: str | None) -> str:
|
|
@@ -733,9 +747,9 @@ class Client:
|
|
|
733
747
|
json: object | None = None,
|
|
734
748
|
params: dict[str, str | int] | None = None,
|
|
735
749
|
retry: bool = False,
|
|
750
|
+
recover_local_connect: bool = True,
|
|
736
751
|
) -> T:
|
|
737
752
|
self._ensure_open()
|
|
738
|
-
self._prepare_endpoint()
|
|
739
753
|
attempts = MAX_RETRY_ATTEMPTS if retry else 1
|
|
740
754
|
last_transport_error: TransportError | None = None
|
|
741
755
|
local_connect_recovery_used = False
|
|
@@ -746,13 +760,14 @@ class Client:
|
|
|
746
760
|
except httpx.RequestError as error:
|
|
747
761
|
last_transport_error = self._connection_error(operation)
|
|
748
762
|
can_recover_local_connect = (
|
|
749
|
-
|
|
763
|
+
recover_local_connect
|
|
764
|
+
and self._config.managed_local
|
|
765
|
+
and self._config.auto_start_local_server
|
|
750
766
|
and isinstance(error, (httpx.ConnectError, httpx.ConnectTimeout))
|
|
751
767
|
and not local_connect_recovery_used
|
|
752
768
|
)
|
|
753
769
|
if can_recover_local_connect:
|
|
754
770
|
local_connect_recovery_used = True
|
|
755
|
-
self._local_ready = False
|
|
756
771
|
self._ensure_local_available()
|
|
757
772
|
if attempt + 1 == attempts:
|
|
758
773
|
attempts += 1
|
|
@@ -760,7 +775,7 @@ class Client:
|
|
|
760
775
|
raise last_transport_error from error
|
|
761
776
|
else:
|
|
762
777
|
self._observe_server_version(response)
|
|
763
|
-
self.
|
|
778
|
+
self._announce_endpoint()
|
|
764
779
|
if response.is_error:
|
|
765
780
|
try:
|
|
766
781
|
api_error = _parse_api_error(response)
|
|
@@ -794,32 +809,55 @@ class Client:
|
|
|
794
809
|
raise AssertionError("Request loop ended without a result or error.")
|
|
795
810
|
raise last_transport_error
|
|
796
811
|
|
|
812
|
+
def _repair_local_connection(self, error: TransportError) -> None:
|
|
813
|
+
"""Repair an opted-in managed-local connection without replaying a request."""
|
|
814
|
+
cause = error.__cause__
|
|
815
|
+
if (
|
|
816
|
+
self._config.managed_local
|
|
817
|
+
and self._config.auto_start_local_server
|
|
818
|
+
and isinstance(cause, (httpx.ConnectError, httpx.ConnectTimeout))
|
|
819
|
+
):
|
|
820
|
+
self._ensure_local_available()
|
|
821
|
+
|
|
797
822
|
def _ensure_open(self) -> None:
|
|
798
823
|
if self._closed:
|
|
799
824
|
raise RuntimeError("Client is closed.")
|
|
800
825
|
|
|
801
826
|
@property
|
|
802
827
|
def _operation_endpoint_details(self) -> dict[str, object]:
|
|
803
|
-
if self._config.
|
|
828
|
+
if self._config.url is not None:
|
|
804
829
|
return {"url": self._config.url}
|
|
805
830
|
return {
|
|
806
|
-
"
|
|
807
|
-
"socket": str(self._config.
|
|
831
|
+
"labtasker_root": str(self._config.labtasker_root),
|
|
832
|
+
"socket": str(self._config.socket),
|
|
808
833
|
}
|
|
809
834
|
|
|
810
|
-
def
|
|
811
|
-
if self.
|
|
812
|
-
self._ensure_local_available()
|
|
813
|
-
|
|
814
|
-
def _announce_http_endpoint(self) -> None:
|
|
815
|
-
if self._endpoint_announced or self._config.local is not None:
|
|
835
|
+
def _announce_endpoint(self) -> None:
|
|
836
|
+
if self._endpoint_announced:
|
|
816
837
|
return
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
838
|
+
if self._config.url is not None:
|
|
839
|
+
transport = self._config.url.partition(":")[0]
|
|
840
|
+
message = (
|
|
841
|
+
f"[labtasker] connected server=remote transport={transport} url={self._config.url}"
|
|
842
|
+
)
|
|
843
|
+
elif self._config.managed_local:
|
|
844
|
+
assert self._config.local is not None and self._config.socket is not None
|
|
845
|
+
pid, version = self._local_ensure_result or (None, None)
|
|
846
|
+
message = (
|
|
847
|
+
"[labtasker] connected server=local transport=unix "
|
|
848
|
+
f"labtasker_root={self._config.labtasker_root} "
|
|
849
|
+
f"database={self._config.local.database} socket={self._config.socket}"
|
|
850
|
+
)
|
|
851
|
+
if pid is not None:
|
|
852
|
+
message += f" pid={pid}"
|
|
853
|
+
if version is not None:
|
|
854
|
+
message += f" version={version}"
|
|
855
|
+
else:
|
|
856
|
+
assert self._config.socket is not None
|
|
857
|
+
message = (
|
|
858
|
+
f"[labtasker] connected server=external transport=unix socket={self._config.socket}"
|
|
859
|
+
)
|
|
860
|
+
print(message, file=sys.stderr)
|
|
823
861
|
self._endpoint_announced = True
|
|
824
862
|
|
|
825
863
|
def _ensure_local_available(self) -> None:
|
|
@@ -827,16 +865,7 @@ class Client:
|
|
|
827
865
|
if paths is None:
|
|
828
866
|
return
|
|
829
867
|
result = ensure_local_server(paths, emit=self._emit_local_transition)
|
|
830
|
-
|
|
831
|
-
version = result.server_version if result.server_version is not None else "unknown"
|
|
832
|
-
print(
|
|
833
|
-
"[labtasker] connected server=local transport=unix "
|
|
834
|
-
f"directory={paths.directory} database={paths.database} socket={paths.socket} "
|
|
835
|
-
f"pid={pid} version={version}",
|
|
836
|
-
file=sys.stderr,
|
|
837
|
-
)
|
|
838
|
-
self._endpoint_announced = True
|
|
839
|
-
self._local_ready = True
|
|
868
|
+
self._local_ensure_result = (result.pid, result.server_version)
|
|
840
869
|
|
|
841
870
|
@staticmethod
|
|
842
871
|
def _emit_local_transition(message: str) -> None:
|
|
@@ -844,16 +873,25 @@ class Client:
|
|
|
844
873
|
|
|
845
874
|
def _connection_error(self, operation: str) -> TransportError:
|
|
846
875
|
details: dict[str, object] = {"operation": operation}
|
|
847
|
-
if self._config.
|
|
876
|
+
if self._config.url is not None:
|
|
848
877
|
details["url"] = self._config.url
|
|
878
|
+
elif not self._config.managed_local:
|
|
879
|
+
details["socket"] = str(self._config.socket)
|
|
849
880
|
else:
|
|
881
|
+
assert self._config.local is not None
|
|
850
882
|
details.update(
|
|
851
883
|
{
|
|
852
884
|
"state": "unhealthy",
|
|
853
|
-
"
|
|
885
|
+
"labtasker_root": str(self._config.labtasker_root),
|
|
854
886
|
"database": str(self._config.local.database),
|
|
855
|
-
"socket": str(self._config.
|
|
887
|
+
"socket": str(self._config.socket),
|
|
856
888
|
"log": str(self._config.local.log),
|
|
889
|
+
"remedies": [
|
|
890
|
+
"rerun with --auto-start-local-server",
|
|
891
|
+
"launch labtasker-server serve --connection socket --daemon "
|
|
892
|
+
f"--labtasker-root {self._config.labtasker_root}",
|
|
893
|
+
"configure LABTASKER_URL or LABTASKER_SOCKET",
|
|
894
|
+
],
|
|
857
895
|
}
|
|
858
896
|
)
|
|
859
897
|
return TransportError("The Labtasker Server could not be reached.", details)
|
|
@@ -29,15 +29,14 @@ from labtasker.tee import configure_worker_logger
|
|
|
29
29
|
from labtasker.types import JSONValue
|
|
30
30
|
from labtasker.validation import validate_identifier, validate_json_object
|
|
31
31
|
from labtasker.worker import (
|
|
32
|
-
POLL_INTERVAL_SECONDS,
|
|
33
32
|
Heartbeat,
|
|
34
33
|
_best_effort_unclaim,
|
|
35
34
|
_ExecutionResult,
|
|
36
35
|
_FailureGuard,
|
|
37
36
|
_finish_journal,
|
|
38
|
-
_generate_run_id,
|
|
39
37
|
_guard_worker_topology,
|
|
40
38
|
_journal_best_effort,
|
|
39
|
+
_next_claim,
|
|
41
40
|
_preflight,
|
|
42
41
|
_report_until_resolved,
|
|
43
42
|
_safe_diagnostic_text,
|
|
@@ -58,6 +57,8 @@ def run_command_worker(
|
|
|
58
57
|
force_stop_timeout: float | None = None,
|
|
59
58
|
max_consecutive_failures: int = 5,
|
|
60
59
|
metadata: dict[str, JSONValue] | None = None,
|
|
60
|
+
labtasker_root: Path | None = None,
|
|
61
|
+
auto_start_local_server: bool = False,
|
|
61
62
|
) -> None:
|
|
62
63
|
guard = _FailureGuard(max_consecutive_failures)
|
|
63
64
|
templates = compile_argv(argv)
|
|
@@ -70,29 +71,26 @@ def run_command_worker(
|
|
|
70
71
|
_guard_command_worker_platform()
|
|
71
72
|
_guard_worker_topology()
|
|
72
73
|
configure_worker_logger()
|
|
73
|
-
with Client(
|
|
74
|
-
|
|
74
|
+
with Client(
|
|
75
|
+
queue=queue,
|
|
76
|
+
labtasker_root=labtasker_root,
|
|
77
|
+
auto_start_local_server=auto_start_local_server,
|
|
78
|
+
) as client:
|
|
79
|
+
queue_name = client._configuration.queue
|
|
75
80
|
_preflight(client, queue_name)
|
|
76
81
|
with ObservationReporter(
|
|
77
|
-
client.
|
|
82
|
+
client._configuration, normalized_route, normalized_metadata
|
|
78
83
|
) as observer:
|
|
79
|
-
idle_deadline: float | None = None
|
|
80
84
|
while True:
|
|
81
|
-
claim =
|
|
85
|
+
claim = _next_claim(
|
|
86
|
+
client,
|
|
82
87
|
route=normalized_route,
|
|
83
|
-
run_id=_generate_run_id(),
|
|
84
88
|
queue=queue_name,
|
|
89
|
+
idle_timeout=normalized_idle_timeout,
|
|
85
90
|
)
|
|
86
91
|
if claim is None:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
idle_deadline = now + normalized_idle_timeout
|
|
90
|
-
if now >= idle_deadline:
|
|
91
|
-
logger.info("Worker idle timeout reached; stopping normally.")
|
|
92
|
-
return
|
|
93
|
-
time.sleep(min(POLL_INTERVAL_SECONDS, idle_deadline - now))
|
|
94
|
-
continue
|
|
95
|
-
idle_deadline = None
|
|
92
|
+
logger.info("Worker idle timeout reached; stopping normally.")
|
|
93
|
+
return
|
|
96
94
|
observer.activity(claim.task.id)
|
|
97
95
|
logger.info(
|
|
98
96
|
"Claimed Task %s as run %s (attempt %d, route %s).",
|
|
@@ -136,9 +134,10 @@ def _run_command_claim(
|
|
|
136
134
|
try:
|
|
137
135
|
journal = LocalRunJournal.create(
|
|
138
136
|
claim=claim,
|
|
139
|
-
endpoint=client.
|
|
137
|
+
endpoint=client._configuration.endpoint_dict(),
|
|
140
138
|
queue=queue,
|
|
141
139
|
route=route,
|
|
140
|
+
labtasker_root=client._configuration.labtasker_root,
|
|
142
141
|
)
|
|
143
142
|
except Exception:
|
|
144
143
|
_best_effort_unclaim(client, claim, queue)
|
|
@@ -510,18 +509,19 @@ def _command_environment(
|
|
|
510
509
|
"LABTASKER_WORKER_ID": worker_id,
|
|
511
510
|
}
|
|
512
511
|
)
|
|
513
|
-
configuration = client.
|
|
514
|
-
if configuration.
|
|
512
|
+
configuration = client._configuration
|
|
513
|
+
if configuration.url is not None:
|
|
515
514
|
assert configuration.url is not None
|
|
516
515
|
environment["LABTASKER_URL"] = configuration.url
|
|
517
516
|
environment.pop("LABTASKER_SOCKET", None)
|
|
518
|
-
environment.pop("LABTASKER_LOCAL_DIRECTORY", None)
|
|
519
517
|
else:
|
|
520
|
-
|
|
521
|
-
environment["
|
|
518
|
+
assert configuration.socket is not None
|
|
519
|
+
environment["LABTASKER_SOCKET"] = str(configuration.socket)
|
|
522
520
|
environment.pop("LABTASKER_URL", None)
|
|
521
|
+
environment.pop("LABTASKER_ROOT", None)
|
|
522
|
+
environment.pop("LABTASKER_LOCAL_DIRECTORY", None)
|
|
523
523
|
token = configuration.token
|
|
524
|
-
if token is None or configuration.
|
|
524
|
+
if token is None or configuration.url is None:
|
|
525
525
|
environment.pop("LABTASKER_TOKEN", None)
|
|
526
526
|
else:
|
|
527
527
|
environment["LABTASKER_TOKEN"] = token
|