Repository navigation
Conversation
_run_worker caught every exception from server.run() and only logged it, so `livekit.agents start` (and `dev`) exited 0 when the worker failed to start (missing/bad credentials, unreachable server, invalid config). Supervisors (systemd, docker, k8s probes, CI) treat that as a clean stop and never alert or restart. Exit non-zero instead, after the usual drain/aclose shutdown. Also stop a secondary shutdown error from masking the real one: when run() fails before the server internals are initialized (e.g. no rtc_session registered), drain()/aclose() raise AttributeError on the uninitialized attributes; that error now stays a warning while the original "worker failed" log remains the reported failure.
|
Thanks for the review β assessed against worktree head 1. The aclose() skip is pre-existing. At head, a non-timeout drain error after 2. Children do not survive β three independent guarantees.
3. Why decline here. The recommended fix wouldn't change child fate (see 2a; where the pool's |
When run() failed and drain() then raised a non-timeout error, the shutdown block downgraded the drain error to a warning but skipped aclose(). That skipped the only teardown path for the resources AgentServer.aclose() alone closes - the inference executor, HTTP session/server, Prometheus server, API client and message channel. aclose() is idempotent and tolerates a failed run(); call it in a separate stage in that path. The path where the worker did not fail keeps its pinned semantics: a drain error still propagates and still skips aclose(). Add a regression test: a stub server whose run() and drain() both raise must still have aclose() invoked, with the drain error remaining a warning and the exit non-zero.
Follow-up: close the worker when
|
What breaks
When the worker fails after startup β missing or bad credentials, an
unreachable LiveKit server, invalid config β the CLI logs the error but the
process exits with code 0. (A failure before the internals initialize,
e.g. no registered
rtc_sessionentrypoint, instead ends in a maskingAttributeError, described below.)The same happens for
devand for the deprecated rich CLI (start/dev).Orchestrators watch the exit code, so they see a clean stop: systemd reports
a successful unit, docker/k8s probes treat the container as exited normally,
and CI passes a job whose worker never came up. Nothing alerts, nothing
restarts.
A second, adjacent problem: when
run()fails before the server internalsare initialized (e.g. no
rtc_sessionregistered), the subsequentserver.drain()raisesAttributeError: 'AgentServer' object has no attribute '_proc_pool'. That secondary error escapes the CLI and becomes the finaltraceback, masking the real failure that had just been logged.
Root cause
_run_workerinlivekit/agents/cli/cli.pywrappedserver.run()in a bareexcept Exceptionthat only logged the failure:The worker task then completed "successfully", the normal drain/
acloseshutdown ran,
_run_workerreturned, andmain()(python -m livekit.agents) finished withreturn 0. The startup failure never reachedthe exit code. In the early-failure case,
drain()at the same site raisedAttributeErroron the uninitialized internals, which then replaced theoriginal error in the final traceback.
The fix
Confined to
_run_worker(livekit/agents/cli/cli.py):_worker_runrecords the failure in addition to logging it.acloseshutdown and loop cleanup, the CLI raisesSystemExit(1)when the worker failed, so every entry point(
python -m livekit.agents start, the deprecated rich CLIstart/dev,and jupyter) reports the failure: the CLI processes exit non-zero; in a
notebook IPython surfaces it as a cell error. The graceful-shutdown path
still runs first.
drain()/aclose()after a failed run islogged as a warning instead of escaping and masking the original error.
When the worker did not fail, such exceptions still propagate exactly
as before (the behavior pinned by
test_drain_timeout.py::test_aclose_skipped_on_non_exitcli_non_timeout_exception).Unchanged on purpose:
run()raisingSystemExit/CancelledError(BaseException) is not caughtby the
except Exception, so signal-driven exits keep their currentsemantics.
Test evidence
New regression tests in
tests/test_cli_exit.py, following the file'sexisting stub-server pattern:
test_worker_startup_failure_exits_nonzero(parametrized overstartanddev): a server whoserun()raises at startup must produce a non-zeroSystemExit, with cleanup still invoked (aclose;drainis skipped inthe
devvariant by pre-existing devmode logic). Failed before the fix(
_run_workerreturned normally β process exit 0), passes after.test_worker_startup_failure_before_init_is_not_masked: a server thatfails before its internals exist, with
drain()/aclose()raisingAttributeErrorlike the realAgentServer. The exit must be a non-zeroSystemExit(not the escapingAttributeError) and the original"worker failed" error must remain the one logged. Failed before the fix
(
AttributeErrorescaped), passes after.The full
tests/test_cli_exit.pyfile passes;tests/test_cli_log_level.py,tests/test_drain_timeout.pyandtests/test_worker_config.py(the othersuites covering the
_run_workercall boundary) show no regressions.