Skip to content

Commit a5b92a2

Browse files
authored
Merge pull request #1927 from transformerlab/fix/stop-cluster-cli
Fix CLI to stop the cluster as well when a job stops
2 parents a01c4f0 + 64fb45a commit a5b92a2

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

cli/src/transformerlab_cli/commands/job.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,39 @@
3131
ACTIVE_JOB_STATUSES = {"RUNNING", "LAUNCHING", "INTERACTIVE", "WAITING"}
3232

3333

34+
def _extract_provider_cluster_from_job(job: dict) -> tuple[str | None, str | None]:
35+
"""Extract provider_id and cluster_name from a job payload."""
36+
if not isinstance(job, dict):
37+
return None, None
38+
job_data = job.get("job_data", {})
39+
if not isinstance(job_data, dict):
40+
return None, None
41+
provider_id = job_data.get("provider_id")
42+
cluster_name = job_data.get("cluster_name")
43+
if not provider_id or not cluster_name:
44+
return None, None
45+
return str(provider_id), str(cluster_name)
46+
47+
48+
def _stop_provider_cluster(experiment_id: str, job_id: str) -> None:
49+
"""Best-effort provider cluster stop to mirror GUI behavior."""
50+
job_response = api.get(f"/experiment/{experiment_id}/jobs/{job_id}")
51+
if job_response.status_code != 200:
52+
return
53+
54+
provider_id, cluster_name = _extract_provider_cluster_from_job(job_response.json())
55+
if not provider_id or not cluster_name:
56+
return
57+
58+
cluster_stop_url = f"/compute_provider/providers/{provider_id}/clusters/{cluster_name}/stop?job_id={job_id}"
59+
cluster_response = api.post(cluster_stop_url)
60+
if cluster_response.status_code >= 400:
61+
console.print(
62+
f"[warning]Warning:[/warning] Job stop requested, but cluster stop request failed "
63+
f"(status {cluster_response.status_code})."
64+
)
65+
66+
3467
def _publish_job_asset(
3568
asset_type: str,
3669
endpoint_collection: str,
@@ -832,6 +865,7 @@ def command_job_stop(
832865
response = api.get(f"/experiment/{current_experiment}/jobs/{job_id}/stop")
833866

834867
if response.status_code == 200:
868+
_stop_provider_cluster(current_experiment, job_id)
835869
console.print(f"[success]✓[/success] Job [bold]{job_id}[/bold] stopped.")
836870
else:
837871
console.print(f"[error]Error:[/error] Failed to stop job. Status code: {response.status_code}")

cli/tests/commands/test_job.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,60 @@ def test_job_info_pretty_still_works(_mock_require, _mock_api):
287287
assert "RUNNING" in out
288288

289289

290+
# ---------------------------------------------------------------------------
291+
# Stop command tests
292+
# ---------------------------------------------------------------------------
293+
294+
295+
@patch("transformerlab_cli.commands.job.api.post")
296+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
297+
@patch("transformerlab_cli.commands.job.api.get")
298+
def test_job_stop_also_sends_cluster_stop_when_provider_metadata_exists(_mock_get, _mock_require, mock_post):
299+
"""job stop should mirror GUI behavior and request provider cluster stop when possible."""
300+
stop_response = MagicMock()
301+
stop_response.status_code = 200
302+
stop_response.json.return_value = {"message": "OK"}
303+
304+
job_response = MagicMock()
305+
job_response.status_code = 200
306+
job_response.json.return_value = {
307+
"id": "42",
308+
"job_data": {"provider_id": "provider-1", "cluster_name": "cluster-a"},
309+
}
310+
311+
_mock_get.side_effect = [stop_response, job_response]
312+
313+
cluster_stop_response = MagicMock()
314+
cluster_stop_response.status_code = 200
315+
mock_post.return_value = cluster_stop_response
316+
317+
result = runner.invoke(app, ["job", "stop", "42"])
318+
assert result.exit_code == 0, result.output
319+
assert _mock_get.call_args_list[0].args[0] == "/experiment/exp1/jobs/42/stop"
320+
assert _mock_get.call_args_list[1].args[0] == "/experiment/exp1/jobs/42"
321+
mock_post.assert_called_once_with("/compute_provider/providers/provider-1/clusters/cluster-a/stop?job_id=42")
322+
323+
324+
@patch("transformerlab_cli.commands.job.api.post")
325+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
326+
@patch("transformerlab_cli.commands.job.api.get")
327+
def test_job_stop_skips_cluster_stop_without_provider_metadata(_mock_get, _mock_require, mock_post):
328+
"""job stop should not call cluster-stop when provider metadata is missing."""
329+
stop_response = MagicMock()
330+
stop_response.status_code = 200
331+
stop_response.json.return_value = {"message": "OK"}
332+
333+
job_response = MagicMock()
334+
job_response.status_code = 200
335+
job_response.json.return_value = {"id": "42", "job_data": {}}
336+
337+
_mock_get.side_effect = [stop_response, job_response]
338+
339+
result = runner.invoke(app, ["job", "stop", "42"])
340+
assert result.exit_code == 0, result.output
341+
mock_post.assert_not_called()
342+
343+
290344
# ---------------------------------------------------------------------------
291345
# Log command tests
292346
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)