Skip to content

Commit 3fce78a

Browse files
committed
Harden Dask error handling payloads and recoverability
1 parent ad62183 commit 3fce78a

6 files changed

Lines changed: 62 additions & 15 deletions

File tree

src/agilab/core/agi-cluster/src/agi_cluster/agi_distributor/api/entrypoint_support.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ def _connection_error_payload(exc: ConnectionError, *, log: Any = logger) -> Dic
140140
return {"status": "error", "message": message, "kind": "connection"}
141141

142142

143+
def _application_error_payload(exc: BaseException, *, kind: str, log: Any = logger) -> Dict[str, str]:
144+
message = str(exc).strip() or "Execution failed."
145+
log.error("AGI.run failed (%s): %s", kind, message)
146+
return {"status": "error", "message": message, "kind": kind}
147+
148+
143149
def _log_unhandled_run_exception(
144150
exc: Exception,
145151
*,
@@ -165,13 +171,15 @@ async def _run_main_with_handled_errors(
165171
try:
166172
return await agi_cls._main(scheduler)
167173
except process_error_type as exc:
168-
log.error("failed to run \n%s", exc)
169-
return None
174+
return _application_error_payload(exc, kind="process", log=log)
170175
except ConnectionError as exc:
171176
return _connection_error_payload(exc, log=log)
172177
except ModuleNotFoundError as exc:
173-
log.error("failed to load module \n%s", exc)
174-
return None
178+
return _application_error_payload(
179+
exc,
180+
kind="module_not_found",
181+
log=log,
182+
)
175183
except _AGI_RUN_BOUNDARY_EXCEPTIONS as exc: # Intentional AGI.run boundary: log and re-raise.
176184
_log_unhandled_run_exception(
177185
exc,

src/agilab/core/agi-cluster/src/agi_cluster/agi_distributor/runtime/runtime_distribution_support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ async def sync(
548548
break
549549

550550
if time_fn() - start_time > timeout:
551-
log.error("Timeout waiting for all workers. {remaining} workers missing.")
551+
log.error("Timeout waiting for all workers. %s workers missing.", remaining)
552552
raise TimeoutError("Timed out waiting for all workers to attach")
553553
await sleep_fn(_sync_poll_delay(poll_attempt))
554554
poll_attempt += 1

src/agilab/core/agi-cluster/src/agi_cluster/agi_distributor/service/service_lifecycle_support.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
OSError,
2929
RuntimeError,
3030
TimeoutError,
31-
TypeError,
32-
ValueError,
3331
)
3432
_SERVICE_BREAK_LOOP_EXCEPTIONS = (
3533
ConnectionError,

src/agilab/core/test/test_agi_distributor_entrypoint_support.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,12 @@ async def _raise_missing(_scheduler):
467467
traceback_format_exc_fn=lambda: "tb",
468468
log=process_log,
469469
)
470-
assert process_result is None
471-
assert process_log.error_messages == ["failed to run \nprocess failed"]
470+
assert process_result == {
471+
"status": "error",
472+
"message": "process failed",
473+
"kind": "process",
474+
}
475+
assert process_log.error_messages == ["AGI.run failed (process): process failed"]
472476

473477
connection_log = _FakeLogger()
474478
connection_result = await entrypoint_support._run_main_with_handled_errors(
@@ -496,8 +500,12 @@ async def _raise_missing(_scheduler):
496500
traceback_format_exc_fn=lambda: "tb",
497501
log=missing_log,
498502
)
499-
assert missing_result is None
500-
assert missing_log.error_messages == ["failed to load module \nmissing module"]
503+
assert missing_result == {
504+
"status": "error",
505+
"message": "missing module",
506+
"kind": "module_not_found",
507+
}
508+
assert missing_log.error_messages == ["AGI.run failed (module_not_found): missing module"]
501509

502510

503511
@pytest.mark.asyncio

src/agilab/core/test/test_agi_distributor_run_api.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ async def _fake_main(_scheduler):
239239

240240

241241
@pytest.mark.asyncio
242-
async def test_agi_run_returns_none_on_process_error(monkeypatch):
242+
async def test_agi_run_returns_process_error_payload(monkeypatch):
243243
env = _minimal_app_env()
244244
env.base_worker_cls = "PandasWorker"
245245

@@ -257,7 +257,11 @@ async def _fake_main(_scheduler):
257257
env,
258258
request=RunRequest(scheduler="127.0.0.1", workers={"127.0.0.1": 1}, mode=AGI.DASK_MODE),
259259
)
260-
assert result is None
260+
assert result == {
261+
"status": "error",
262+
"message": "process failed",
263+
"kind": "process",
264+
}
261265

262266

263267
@pytest.mark.asyncio
@@ -280,7 +284,7 @@ async def _boom(_scheduler):
280284

281285

282286
@pytest.mark.asyncio
283-
async def test_agi_run_returns_none_on_module_not_found(monkeypatch):
287+
async def test_agi_run_returns_module_not_found_payload(monkeypatch):
284288
env = _minimal_app_env()
285289
env.base_worker_cls = "PandasWorker"
286290
monkeypatch.setattr(AGI, "_train_capacity", staticmethod(lambda *_args, **_kwargs: None))
@@ -289,7 +293,11 @@ async def _missing(_scheduler):
289293
raise ModuleNotFoundError("missing module")
290294

291295
monkeypatch.setattr(AGI, "_main", staticmethod(_missing))
292-
assert await AGI.run(env, request=RunRequest(workers={"127.0.0.1": 1}, mode=AGI.DASK_MODE)) is None
296+
assert await AGI.run(env, request=RunRequest(workers={"127.0.0.1": 1}, mode=AGI.DASK_MODE)) == {
297+
"status": "error",
298+
"message": "missing module",
299+
"kind": "module_not_found",
300+
}
293301

294302

295303
@pytest.mark.asyncio

src/agilab/core/test/test_agi_distributor_service_lifecycle_support.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,31 @@ def _boom(_args):
469469
await AGI._service_recover(env, allow_stale_cleanup=True)
470470

471471

472+
@pytest.mark.asyncio
473+
async def test_service_recover_propagates_unexpected_value_error(monkeypatch):
474+
env = AgiEnv(apps_path=Path("src/agilab/apps/builtin"), app="minimal_app_project", verbose=0)
475+
AGI._service_write_state(
476+
env,
477+
{
478+
"schema": "agi.service.state.v1",
479+
"target": env.target,
480+
"app": env.app,
481+
"mode": AGI.DASK_MODE,
482+
"run_type": "run --no-sync",
483+
"workers": {"127.0.0.1": 1},
484+
"args": {"sample": 1},
485+
},
486+
)
487+
488+
def _boom(_args):
489+
raise ValueError("unexpected service arg mapping")
490+
491+
monkeypatch.setattr(AGI, "_service_public_args", staticmethod(_boom))
492+
493+
with pytest.raises(ValueError, match="unexpected service arg mapping"):
494+
await AGI._service_recover(env, allow_stale_cleanup=True)
495+
496+
472497
@pytest.mark.asyncio
473498
async def test_service_recover_fails_when_no_workers_attached(monkeypatch):
474499
env = AgiEnv(apps_path=Path("src/agilab/apps/builtin"), app="minimal_app_project", verbose=0)

0 commit comments

Comments
 (0)