Skip to content

Commit 285b79b

Browse files
author
Homer Quan
committed
auto: update all repos
1 parent 42f883f commit 285b79b

6 files changed

Lines changed: 303 additions & 31 deletions

File tree

mn_cli/libs/job_cleanup.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,31 @@
2020
_RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
2121

2222

23+
class JobResourceCleanupError(RuntimeError):
24+
"""Raised when a cleared job still has adapter-owned local resources."""
25+
26+
2327
def cleanup_cancelled_job_resources(
2428
job_id: str, *, runtime_client: Any, log: Any
25-
) -> None:
29+
) -> dict[str, list[str]]:
30+
summary: dict[str, list[str]] = {
31+
"process_removed": [],
32+
"process_skipped": [],
33+
"errors": [],
34+
}
2635
original_job_id = job_id
2736
job_id = _validated_resource_id(job_id)
2837
if job_id is None:
2938
log.warning(
3039
"Refusing local cleanup for invalid job ID: %r", original_job_id
3140
)
32-
return
41+
summary["errors"].append(f"invalid job ID: {original_job_id!r}")
42+
return summary
3343

34-
summary = {"process_removed": [], "process_skipped": [], "errors": []}
3544
run_id = blueprint_run_id_for_job(job_id, runtime_client=runtime_client)
3645
if run_id:
3746
run_dir = default_runs_root() / run_id
38-
if run_dir.is_dir():
47+
if run_dir.is_dir() and not run_dir.is_symlink():
3948
cleanup_blueprint_host_hooks(
4049
run_dir, dry_run=False, summary=summary, reason="job_cancelled"
4150
)
@@ -46,6 +55,7 @@ def cleanup_cancelled_job_resources(
4655
cleanup_local_openshell_sandboxes(job_id, summary)
4756
for error in summary["errors"]:
4857
log.warning("Failed to cleanup local resources for job %s: %s", job_id, error)
58+
return summary
4959

5060

5161
def cleanup_cleared_job_resources(
@@ -58,13 +68,22 @@ def cleanup_cleared_job_resources(
5868
log.warning(
5969
"Refusing local cleanup for invalid job ID: %r", original_job_id
6070
)
61-
return
71+
raise JobResourceCleanupError(f"invalid job ID: {original_job_id!r}")
6272

6373
run_id = blueprint_run_id_for_job(job_id, runtime_client=runtime_client)
64-
cleanup_cancelled_job_resources(job_id, runtime_client=runtime_client, log=log)
74+
cancelled_summary = (
75+
cleanup_cancelled_job_resources(
76+
job_id, runtime_client=runtime_client, log=log
77+
)
78+
or {}
79+
)
80+
errors = list(cancelled_summary.get("errors") or [])
6581
try:
66-
cleanup_docker_worker_services(job_id=job_id)
67-
except Exception:
82+
docker_result = cleanup_docker_worker_services(job_id=job_id)
83+
if isinstance(docker_result, dict):
84+
errors.extend(str(error) for error in docker_result.get("errors") or [])
85+
except Exception as error:
86+
errors.append(f"DockerWorker cleanup failed: {error}")
6887
log.warning(
6988
"Failed to cleanup DockerWorker resources for cleared job %s",
7089
job_id,
@@ -82,12 +101,21 @@ def cleanup_cleared_job_resources(
82101

83102
for path in paths:
84103
try:
85-
shutil.rmtree(path)
104+
if path.is_symlink():
105+
path.unlink()
106+
else:
107+
shutil.rmtree(path)
86108
except FileNotFoundError:
87109
pass
88-
except OSError:
110+
except OSError as error:
111+
errors.append(f"failed to remove {path}: {error}")
89112
log.warning("Failed to remove cleared job path %s", path, exc_info=True)
90113

114+
if errors:
115+
raise JobResourceCleanupError(
116+
f"local cleanup incomplete for {job_id}: {'; '.join(errors)}"
117+
)
118+
91119

92120
def blueprint_run_id_for_job(job_id: str, *, runtime_client: Any) -> str | None:
93121
job_id = _validated_resource_id(job_id)

mn_cli/libs/job_cmds.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mn_cli.shared import console, client, config, logger
88
from mn_cli.error_handler import handle_cli_error
99
from mn_cli.libs.job_cleanup import (
10+
JobResourceCleanupError,
1011
blueprint_run_id_for_job,
1112
blueprint_run_id_from_run_store,
1213
cleanup_cancelled_job_resources,
@@ -115,23 +116,36 @@ def clear(
115116
yes: bool = typer.Option(False, "--yes", "-y", help="Clear terminal jobs without prompting."),
116117
):
117118
"""Remove terminal jobs and all runtime resources they own."""
119+
cleanup_errors: list[str] = []
120+
121+
def cleanup_item(event: dict) -> None:
122+
job_id = str(event.get("item_id") or "")
123+
try:
124+
_cleanup_cleared_job_resources(job_id)
125+
except JobResourceCleanupError as error:
126+
cleanup_errors.append(str(error))
127+
event["status"] = "failed"
128+
event["error"] = f"local runtime cleanup incomplete: {error}"
129+
logger.error("Local cleanup failed for cleared job %s: %s", job_id, error)
130+
118131
try:
119132
if not yes and not typer.confirm(
120133
"Clear all terminal jobs and their runtime resources?", default=False
121134
):
122135
print_confirmed(console, "Job clear", status="aborted")
123136
return
124137

138+
_cleanup_job_ids_or_raise(_terminal_job_ids())
125139
result = start_and_watch(
126140
"clear_jobs",
127141
{},
128142
action="Job clear",
129-
on_accepted_item=lambda event: _cleanup_cleared_job_resources(
130-
str(event.get("item_id") or "")
131-
),
143+
on_accepted_item=cleanup_item,
132144
runtime_client=client,
133145
)
134146
logger.info("Finished clear operation %s", result.get("operation_id"))
147+
if cleanup_errors:
148+
raise JobResourceCleanupError("; ".join(cleanup_errors))
135149
except grpc.RpcError as e:
136150
if e.code() == grpc.StatusCode.PERMISSION_DENIED and "MN_GRPC_ADMIN_TOKEN" in str(e.details()):
137151
print_error(console, "ClearJobs admin authorization failed.")
@@ -242,6 +256,34 @@ def _cleanup_cleared_job_resources(job_id: str) -> None:
242256
cleanup_cleared_job_resources(job_id, runtime_client=client, log=logger)
243257

244258

259+
def _cleanup_job_ids_or_raise(job_ids: list[str]) -> None:
260+
errors = []
261+
for job_id in job_ids:
262+
try:
263+
_cleanup_cleared_job_resources(job_id)
264+
except JobResourceCleanupError as error:
265+
errors.append(str(error))
266+
if errors:
267+
raise JobResourceCleanupError("; ".join(errors))
268+
269+
270+
def _terminal_job_ids() -> list[str]:
271+
result = json.loads(
272+
client.list_jobs(limit=_ALL_JOBS_LIMIT, include_terminal=True)
273+
)
274+
jobs = result.get("data") if isinstance(result, dict) else None
275+
if not isinstance(jobs, list):
276+
raise ValueError("runtime returned an invalid job list")
277+
return [
278+
job_id
279+
for job in jobs
280+
if isinstance(job, dict)
281+
and job.get("status") in {"completed", "failed", "cancelled"}
282+
and isinstance((job_id := job.get("job_id")), str)
283+
and job_id
284+
]
285+
286+
245287
def _blueprint_run_id_for_job(job_id: str) -> str | None:
246288
return blueprint_run_id_for_job(job_id, runtime_client=client)
247289

mn_cli/libs/operation_cmds.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ def _print_plain_event(
172172
if event_type == "stream_heartbeat":
173173
return
174174

175+
if on_accepted_item and _accepted_item_status(event.get("status")):
176+
on_accepted_item(event)
177+
175178
item_id = str(event.get("item_id") or "operation")
176179
status = str(event.get("status") or "")
177180

@@ -180,13 +183,9 @@ def _print_plain_event(
180183
elif status == "failed":
181184
print_warning(console, f"{item_id}: {event.get('error') or 'operation item failed'}")
182185
elif status == "cancellation_pending":
183-
if on_accepted_item:
184-
on_accepted_item(event)
185186
print_info(console, f"{item_id}: cancellation accepted; cleanup queued on owner node")
186187
elif event_type in {"item_completed", "item_deferred"}:
187188
prefix = "✓" if status in _SUCCESS_ITEM_STATUSES else "→"
188-
if on_accepted_item and _accepted_item_status(status):
189-
on_accepted_item(event)
190189
console.print(f"{prefix} {item_id}: {status or 'completed'}")
191190

192191

mn_cli/libs/stable_job_cmds.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from mn_cli.error_handler import handle_cli_error
1010
from mn_cli.libs.bundles import read_bundle
11-
from mn_cli.libs.job_cleanup import cleanup_cleared_job_resources
11+
from mn_cli.libs.job_cleanup import JobResourceCleanupError, cleanup_cleared_job_resources
1212
from mn_cli.libs.ui import print_success_confirmation
1313
from mn_cli.shared import client, console, logger
1414

@@ -102,10 +102,23 @@ def delete(
102102
return
103103
try:
104104
run_ids = _stable_job_run_ids(job_id)
105+
cleanup_errors = []
106+
for resource_id in [*run_ids, job_id]:
107+
try:
108+
cleanup_cleared_job_resources(
109+
resource_id, runtime_client=client, log=logger
110+
)
111+
except JobResourceCleanupError as error:
112+
cleanup_errors.append(str(error))
113+
if cleanup_errors:
114+
raise JobResourceCleanupError("; ".join(cleanup_errors))
115+
105116
result = json.loads(client.delete_stable_job(job_id, confirmed=True))
106-
for run_id in run_ids:
107-
cleanup_cleared_job_resources(run_id, runtime_client=client, log=logger)
108-
cleanup_cleared_job_resources(job_id, runtime_client=client, log=logger)
117+
cleanup_errors = [
118+
str(error) for error in result.get("resource_cleanup_errors") or []
119+
]
120+
if cleanup_errors:
121+
raise JobResourceCleanupError("; ".join(cleanup_errors))
109122
console.print_json(data=result)
110123
except Exception as exc:
111124
handle_cli_error(exc, console, "job delete")

0 commit comments

Comments
 (0)