jambu-gpu 0.4.0__tar.gz → 0.4.1__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.
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/PKG-INFO +1 -1
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/CONFIG.md +1 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/INTEGRATION.md +1 -1
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu.example.yaml +1 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/__init__.py +1 -1
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/config.py +11 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/engine.py +33 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/pyproject.toml +1 -1
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/fakes.py +8 -1
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_config.py +8 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_engine.py +72 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/.env.example +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/.gitignore +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/CONTRIBUTING.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/LICENSE +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/README.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/ACCEPTANCE.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/ADDING_A_PROVIDER.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/AGENTS.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/CHOOSING_A_MODEL.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/LIFECYCLE.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/USAGE.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/docs/assets/jambu-logo.jpg +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/experiment.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/jambu.yaml +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/README.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/package-lock.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/package.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/src/agent.ts +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/src/index.ts +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/src/tools/weather-tool.ts +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/mastra-agent/tsconfig.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/README.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/package-lock.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/package.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/src/index.ts +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/src/jambuGpu.ts +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/examples/typescript-client/tsconfig.json +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu.models.yaml +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu.yaml +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/__main__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/agent/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/agent/install.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/agent/jobs.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/agent/mcp.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/agent/skill/SKILL.md +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/_common.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/account.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/agent.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/inspect.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/lifecycle.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/main.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/providers.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/run.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/setup.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/status.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/timer.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/cli/validate.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/beacon.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/credentials.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/durations.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/errors.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/events.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/execution.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/health.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/hosts.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/model_inspect.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/models.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/paths.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/policy.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/state.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/sweep.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/core/tool_parsers.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/base.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/registry.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/vast/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/vast/client.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/vast/config.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/vast/mapper.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/providers/vast/provider.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/runtime/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/runtime/base.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/runtime/vllm.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/watchdog/__init__.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/watchdog/agent_body.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/jambu_gpu/watchdog/builder.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/conftest.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_agent.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_cli.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_engine_sessions.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_integration.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_model_inspect.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_paths.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_policy.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_runtime_vllm.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_state.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_sweep.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_vast_mapper.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_vast_provider.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/tests/test_watchdog.py +0 -0
- {jambu_gpu-0.4.0 → jambu_gpu-0.4.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jambu-gpu
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Provider-agnostic CLI for provisioning and managing temporary GPU runtimes
|
|
5
5
|
Project-URL: Homepage, https://github.com/jambuai/jambu-gpu
|
|
6
6
|
Project-URL: Repository, https://github.com/jambuai/jambu-gpu
|
|
@@ -205,6 +205,7 @@ judged by the sweep's own `--orphan-max-age` and `--stopped-ttl` flags. See
|
|
|
205
205
|
| --- | --- | --- |
|
|
206
206
|
| `interval` | `60s` | Probe interval while waiting for the runtime. |
|
|
207
207
|
| `boot_timeout` | `30m` | How long the machine may take to reach running, mostly the image pull. Past it the host counts as failed and `setup` tries another (`compute.host_attempts`). |
|
|
208
|
+
| `pull_stall_timeout` | `10m` | How long the image pull may go without progress (no new line from the provider, or only retries of a layer) before the host counts as failed and is replaced, without waiting out `boot_timeout`. Healthy pulls measured live showed a new line at least every 8 minutes; stalled ones never recovered. `off` disables it. |
|
|
208
209
|
| `startup_timeout` | `10m` | Once the machine runs, how long the model server may take to answer. Raise it for very large models. |
|
|
209
210
|
| `path` | `/health` | Health path on the model endpoint. |
|
|
210
211
|
|
|
@@ -117,7 +117,7 @@ gpu env --format dotenv > .env.gpu
|
|
|
117
117
|
|
|
118
118
|
| Event | What the app sees | What it does |
|
|
119
119
|
| --- | --- | --- |
|
|
120
|
-
| first `setup` | blocks while a machine is rented, the image pulled and the model downloaded: usually 5–15 minutes. A bad host (stalled image pull, broken network) is destroyed and replaced automatically, each costing up to `health.boot_timeout` (30 min), at most `compute.host_attempts` (3) times | wait; do not put a short timeout on `setup`, it bounds itself |
|
|
120
|
+
| first `setup` | blocks while a machine is rented, the image pulled and the model downloaded: usually 5–15 minutes. A bad host (stalled image pull, broken network) is destroyed and replaced automatically, each costing up to `health.pull_stall_timeout` (10 min) when its pull stalls, or `health.boot_timeout` (30 min) when it crawls, at most `compute.host_attempts` (3) times | wait; do not put a short timeout on `setup`, it bounds itself |
|
|
121
121
|
| idle for `idle_timeout` | the watchdog stops the machine; requests fail with connection refused or a timeout | call `setup` again and retry the request |
|
|
122
122
|
| `setup` on a stopped machine | it is restarted: no new rent, no model download, under a minute for a small model (41 s measured) | wait, then use the **new** `base_url`: the provider may assign a new IP and port on restart |
|
|
123
123
|
| stopped for `stopped_ttl` | the machine is destroyed to stop the storage bill | the next `setup` rents a new one (and a new key) |
|
|
@@ -230,12 +230,17 @@ class HealthConfig(_Base):
|
|
|
230
230
|
interval: str = "60s"
|
|
231
231
|
# How long the machine may take to reach running (mostly the image pull).
|
|
232
232
|
boot_timeout: str = "30m"
|
|
233
|
+
# How long that pull may go without progress before the host counts as
|
|
234
|
+
# stalled; a stalled pull never resumes, so waiting out boot_timeout
|
|
235
|
+
# only bills the dead host longer.
|
|
236
|
+
pull_stall_timeout: str = "10m"
|
|
233
237
|
# How long the model server may then take to answer its health check.
|
|
234
238
|
startup_timeout: str = "10m"
|
|
235
239
|
path: str = "/health"
|
|
236
240
|
|
|
237
241
|
_v_interval = _duration("interval")
|
|
238
242
|
_v_boot = _duration("boot_timeout")
|
|
243
|
+
_v_stall = _duration("pull_stall_timeout")
|
|
239
244
|
_v_startup = _duration("startup_timeout")
|
|
240
245
|
|
|
241
246
|
|
|
@@ -310,6 +315,10 @@ class RuntimeConfig(_Base):
|
|
|
310
315
|
def boot_timeout_s(self) -> float:
|
|
311
316
|
return parse_duration(self.health.boot_timeout) or 1800.0
|
|
312
317
|
|
|
318
|
+
@property
|
|
319
|
+
def pull_stall_timeout_s(self) -> Optional[float]:
|
|
320
|
+
return parse_duration(self.health.pull_stall_timeout)
|
|
321
|
+
|
|
313
322
|
@property
|
|
314
323
|
def startup_timeout_s(self) -> float:
|
|
315
324
|
return parse_duration(self.health.startup_timeout) or 600.0
|
|
@@ -579,6 +588,7 @@ gpu_runtime:
|
|
|
579
588
|
health:
|
|
580
589
|
interval: 60s
|
|
581
590
|
boot_timeout: 30m
|
|
591
|
+
pull_stall_timeout: 10m
|
|
582
592
|
startup_timeout: 10m
|
|
583
593
|
|
|
584
594
|
budget:
|
|
@@ -628,6 +638,7 @@ lifecycle:
|
|
|
628
638
|
health:
|
|
629
639
|
interval: 60s
|
|
630
640
|
boot_timeout: 30m
|
|
641
|
+
pull_stall_timeout: 10m
|
|
631
642
|
startup_timeout: 10m
|
|
632
643
|
|
|
633
644
|
budget:
|
|
@@ -61,6 +61,9 @@ _HOST_NETWORK_FAILURE = re.compile(
|
|
|
61
61
|
r"Temporary failure in name resolution|Network is unreachable"
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
+
# A pull stuck retrying a layer: its countdown changes, but it is no progress.
|
|
65
|
+
_PULL_RETRY = re.compile(r"\bretrying\b", re.IGNORECASE)
|
|
66
|
+
|
|
64
67
|
# Where `gpu logs --source` reads from: the watchdog serves the instance files.
|
|
65
68
|
LOG_SOURCES = ("runtime", "onstart", "watchdog", "provider")
|
|
66
69
|
# Runtime log lines quoted in the error when the model server dies at boot.
|
|
@@ -74,6 +77,19 @@ def _noop(_message: str) -> None:
|
|
|
74
77
|
return None
|
|
75
78
|
|
|
76
79
|
|
|
80
|
+
def _pull_progress(message: Optional[str]) -> Optional[str]:
|
|
81
|
+
"""The provider's boot message when it shows the pull moving, else None.
|
|
82
|
+
|
|
83
|
+
Vast reports the image pull's latest line (``<layer>: Download complete``,
|
|
84
|
+
``<layer>: Pull complete``). A new line means a layer moved; the same line,
|
|
85
|
+
no line, or a retry countdown means it did not.
|
|
86
|
+
"""
|
|
87
|
+
text = (message or "").strip()
|
|
88
|
+
if not text or _PULL_RETRY.search(text):
|
|
89
|
+
return None
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
77
93
|
class Engine:
|
|
78
94
|
def __init__(
|
|
79
95
|
self,
|
|
@@ -779,8 +795,13 @@ class Engine:
|
|
|
779
795
|
instance = state.instance
|
|
780
796
|
assert instance is not None
|
|
781
797
|
boot_timeout = self.config.boot_timeout_s
|
|
798
|
+
stall_timeout = self.config.pull_stall_timeout_s
|
|
782
799
|
deadline = time.time() + boot_timeout
|
|
783
800
|
announced = False
|
|
801
|
+
# Live, healthy pulls printed a new line every 1-8 minutes; stalled ones
|
|
802
|
+
# froze (or retried one layer) until boot_timeout, 30 minutes per host.
|
|
803
|
+
progress: Optional[str] = None
|
|
804
|
+
progress_at = time.time()
|
|
784
805
|
while time.time() < deadline:
|
|
785
806
|
status = self.provider.status(instance)
|
|
786
807
|
if not status.exists:
|
|
@@ -810,6 +831,18 @@ class Engine:
|
|
|
810
831
|
InstanceState.IDLE,
|
|
811
832
|
) and (have_ports or not ready_ports):
|
|
812
833
|
return current
|
|
834
|
+
moved = _pull_progress(status.message)
|
|
835
|
+
if moved is not None and moved != progress:
|
|
836
|
+
progress, progress_at = moved, time.time()
|
|
837
|
+
elif stall_timeout and time.time() - progress_at >= stall_timeout:
|
|
838
|
+
last = (status.message or "").strip().splitlines()
|
|
839
|
+
raise HostFailure(
|
|
840
|
+
f"instance {instance.id}'s image pull made no progress for "
|
|
841
|
+
f"{format_duration(stall_timeout)} (health.pull_stall_timeout)"
|
|
842
|
+
+ (f"; last: {last[-1][:160]}" if last else ""),
|
|
843
|
+
reason=f"image pull stalled for {format_duration(stall_timeout)}",
|
|
844
|
+
instance=current,
|
|
845
|
+
)
|
|
813
846
|
if not announced:
|
|
814
847
|
announced = True
|
|
815
848
|
self.echo(" waiting for the machine to come up ...")
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Callable, Optional
|
|
7
7
|
|
|
8
8
|
from jambu_gpu.core.errors import CapacityUnavailableError, ProvisioningError
|
|
9
9
|
from jambu_gpu.core.models import (
|
|
@@ -66,6 +66,10 @@ class FakeProvider(GPUProvider):
|
|
|
66
66
|
# reaches running).
|
|
67
67
|
self.failing_hosts: set[str] = set()
|
|
68
68
|
self.host_failure = "failed"
|
|
69
|
+
# What a "loading" machine reports on its Nth status poll, as Vast
|
|
70
|
+
# reports the image pull's latest line.
|
|
71
|
+
self.boot_message: Optional[Callable[[int], str]] = None
|
|
72
|
+
self.boot_polls = 0
|
|
69
73
|
self.rented_hosts: list[str] = []
|
|
70
74
|
self.searches: list[list[str]] = []
|
|
71
75
|
|
|
@@ -169,6 +173,9 @@ class FakeProvider(GPUProvider):
|
|
|
169
173
|
if stored is None:
|
|
170
174
|
return InstanceStatus(state=InstanceState.DESTROYED, exists=False)
|
|
171
175
|
message = ""
|
|
176
|
+
if stored.state is InstanceState.STARTING and self.boot_message is not None:
|
|
177
|
+
message = self.boot_message(self.boot_polls)
|
|
178
|
+
self.boot_polls += 1
|
|
172
179
|
if stored.state is InstanceState.FAILED:
|
|
173
180
|
message = (
|
|
174
181
|
'Error response from daemon: Get "https://registry-1.docker.io/v2/": '
|
|
@@ -388,3 +388,11 @@ def test_host_attempts_and_boot_timeout_keep_the_instance_compatible(write_confi
|
|
|
388
388
|
base = write_config().fingerprint()
|
|
389
389
|
assert write_config(compute={"host_attempts": 5}).fingerprint() == base
|
|
390
390
|
assert write_config(health={"boot_timeout": "40m"}).fingerprint() == base
|
|
391
|
+
assert write_config(health={"pull_stall_timeout": "5m"}).fingerprint() == base
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_pull_stall_timeout_defaults_to_ten_minutes_and_can_be_off(write_config):
|
|
395
|
+
assert write_config().pull_stall_timeout_s == 10 * 60
|
|
396
|
+
assert write_config(health={"pull_stall_timeout": "off"}).pull_stall_timeout_s is None
|
|
397
|
+
with pytest.raises(ConfigError, match="pull_stall_timeout"):
|
|
398
|
+
write_config(health={"pull_stall_timeout": "soon"})
|
|
@@ -720,6 +720,78 @@ def test_a_stuck_image_pull_times_out_on_boot_timeout_not_startup_timeout(
|
|
|
720
720
|
assert health_waits == [420.0]
|
|
721
721
|
|
|
722
722
|
|
|
723
|
+
def _with_clock(monkeypatch, start=1_000_000.0):
|
|
724
|
+
"""Make the boot wait's sleeps advance a fake clock: minutes in no time."""
|
|
725
|
+
clock = [start]
|
|
726
|
+
monkeypatch.setattr("jambu_gpu.core.engine.time.time", lambda: clock[0])
|
|
727
|
+
monkeypatch.setattr(
|
|
728
|
+
"jambu_gpu.core.engine.time.sleep",
|
|
729
|
+
lambda seconds: clock.__setitem__(0, clock[0] + seconds),
|
|
730
|
+
)
|
|
731
|
+
return clock
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def test_a_frozen_image_pull_is_given_up_long_before_boot_timeout(write_config, monkeypatch):
|
|
735
|
+
"""Live: three hosts froze on one pull line until the 30-minute boot_timeout."""
|
|
736
|
+
engine, lines, _ = _booting(write_config, monkeypatch)
|
|
737
|
+
clock = _with_clock(monkeypatch)
|
|
738
|
+
engine.provider.fail_first_hosts(1, how="loading")
|
|
739
|
+
engine.provider.boot_message = lambda poll: (
|
|
740
|
+
"995efd0b979d: Verifying Checksum" if poll < 20 else "067869a57bf4: Pull complete"
|
|
741
|
+
)
|
|
742
|
+
started = clock[0]
|
|
743
|
+
|
|
744
|
+
state = engine.setup()
|
|
745
|
+
|
|
746
|
+
assert state.instance.host_id == "m2"
|
|
747
|
+
assert "! host m1 failed (image pull stalled for 10m); trying another machine (2/3)" in lines
|
|
748
|
+
# Two minutes of progress, then ten without: replaced at about 12, not 30.
|
|
749
|
+
assert 12 * 60 <= clock[0] - started < 13 * 60
|
|
750
|
+
assert engine.provider.searches[-1] == ["m1"], "the stalled host is not rented again"
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def test_a_pull_retrying_one_layer_counts_as_stalled(write_config, monkeypatch):
|
|
754
|
+
engine, lines, _ = _booting(write_config, monkeypatch)
|
|
755
|
+
clock = _with_clock(monkeypatch)
|
|
756
|
+
engine.provider.fail_first_hosts(1, how="loading")
|
|
757
|
+
engine.provider.boot_message = lambda poll: (
|
|
758
|
+
f"60c68e5ca95a: Retrying in {1 + poll % 3} second{'s' if poll % 3 else ''}"
|
|
759
|
+
)
|
|
760
|
+
started = clock[0]
|
|
761
|
+
|
|
762
|
+
engine.setup()
|
|
763
|
+
|
|
764
|
+
assert "! host m1 failed (image pull stalled for 10m); trying another machine (2/3)" in lines
|
|
765
|
+
assert 10 * 60 <= clock[0] - started < 11 * 60
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_a_slow_pull_that_keeps_moving_waits_for_boot_timeout(write_config, monkeypatch):
|
|
769
|
+
"""A new layer every 8 minutes is slow, not stuck: only boot_timeout ends it."""
|
|
770
|
+
engine, lines, _ = _booting(write_config, monkeypatch)
|
|
771
|
+
clock = _with_clock(monkeypatch)
|
|
772
|
+
engine.provider.fail_first_hosts(1, how="loading")
|
|
773
|
+
engine.provider.boot_message = lambda poll: f"layer{poll * 6 // 480}: Pull complete"
|
|
774
|
+
started = clock[0]
|
|
775
|
+
|
|
776
|
+
engine.setup()
|
|
777
|
+
|
|
778
|
+
assert "! host m1 failed (image pull timed out after 30m); trying another machine (2/3)" in lines
|
|
779
|
+
assert clock[0] - started >= 30 * 60
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_pull_stall_detection_can_be_turned_off(write_config, monkeypatch):
|
|
783
|
+
engine, lines, _ = _booting(
|
|
784
|
+
write_config, monkeypatch, health={"pull_stall_timeout": "off", "boot_timeout": "20m"}
|
|
785
|
+
)
|
|
786
|
+
_with_clock(monkeypatch)
|
|
787
|
+
engine.provider.fail_first_hosts(1, how="loading")
|
|
788
|
+
engine.provider.boot_message = lambda poll: "067869a57bf4: Pull complete"
|
|
789
|
+
|
|
790
|
+
engine.setup()
|
|
791
|
+
|
|
792
|
+
assert "! host m1 failed (image pull timed out after 20m); trying another machine (2/3)" in lines
|
|
793
|
+
|
|
794
|
+
|
|
723
795
|
def test_a_model_server_that_dies_is_not_blamed_on_the_host(engine, monkeypatch):
|
|
724
796
|
from jambu_gpu.core.errors import HostFailure, RuntimeStartupError
|
|
725
797
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|