[deployment,core] fix: harden local sandbox lifecycle#82
Open
FeathBow wants to merge 1 commit into
Open
Conversation
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Hardens the
LocalDeploymentsandbox lifecycle for high-concurrency RL training, mirroring the Modal hardening from #47. Refs #43; it does not claim to close it. Blast radius:uni_agent/deployment/local/deployment.pyplusdiagnose=Falseon the two loguru sinks inasync_logging.py.Checklist Before Starting
deployment.py: it overlaps_start_oci_containerand adjacent endpoint/network code, so whichever lands second must rebase; this branch keeps_build_run_commanditself byte-identical tomain, so [deployment,docs] fix: select local OCI runtime endpoint by network path #44's change to that function still grafts cleanly -- merge [deployment,docs] fix: select local OCI runtime endpoint by network path #44 first and rebase this onto it), [train] fix: add Layer-2 safety net and MoE shape match to UniAgentLoop failure path #50 (no intersection:agent_loop.pyis atmainhere), [core] fix: replace per-run log sinks with single dispatch sink to fix leaks and HDFS stalls #63 (the onlyasync_logging.pychange here isdiagnose=Falseon the two sink configs; the dispatch rework is untouched)[{modules}] {type}: {description}(checked by CI){modules}may includecore,interaction,model,env,tools,deployment,reward,dashboard,docs,examples,data,train,ci,build,deps,misc,like[interaction, tools, docs]{type}must be one offeat,fix,refactor,chore,test[BREAKING]to the beginning of the title[1/N][BREAKING][deployment, docs] feat: simplify runtime env configurationTest
tests/deployment/unit tests: 101 cases on this branch (the 2 config tests land in PR 2).PYTHONPATH=. pytest tests/deployment/. Python 3.11 venv. No container runtime is needed -- the tests replacesubprocess.run/Popenwith doubles, so no realdocker/apptaineris ever invoked (an autouse fixture also stubs free-port picking). Most cases aim at failures the Docker gate cannot stage reliably (a CLI killed mid-create, a tag query that will not answer, aclose()that hangs); the rest pin plain behaviour (command construction, config validation, the limiter's plumbing).pre-commit run --all-filesclean (ruff, ruff-format, mypy, compileall).Design & Code Changes
docker run, so its future (_future) holds a single slot until someone takes the result back;_exec_bg,start(), andstop()all refuse to run over an unclaimed one, and every event-loop CLI call (run/logs/rm) goes through it so normovertakes the log fetch that must precede it. A start happens only over a clean deployment:start()refuses unless_has_cleanup()is false (nothing owned, in flight, unnamed, a retained runtime handle, or an unreaped process/log). The failed-attempt cleanup between retries is bounded by_CLEANWAIT, but therm -fit launches carries a larger bound (2 x _RMWAIT + _RMPAUSE) on an uncancellable thread -- so cleanup can return having reclaimed nothing, and a retry that started anyway would turn one failed cleanup into two live containers; the loop stops and demands an explicitstop(). (The GC finalizer is the exception: it shells out directly, each OCI call bounded by_DELWAIT, best-effort.)--cidfileand a randomuni-agent.owner=<uuid>label, injected in_with_cidafter every user option soextra_run_argscannot override them (so_build_run_commandstays byte-identical tomain); an untaggable shape or an ambiguous bare--is refused rather than launched. The docker CLI creates the cidfile empty, the daemon creates the container, and only then does the CLI write the id -- so a failed id-write exits non-zero and deletes the cidfile again: "non-zero, no cidfile, no stdout" can still be a live container, and the owner label is then the only ownership-safe way left to find it. Only a full 64-hex id is accepted; with none, the owner tag decides -- a single match is removed by id; two cases prove nothing of ours exists (the runtime never ran, or it ran and reported failure with an authoritative-empty query); every unresolved outcome is the state the container may exist and we cannot name it, kept, reported at ERROR, and blocking the retry and the nextstart()untilstop()or the finalizer re-probes and removes by id. An id leaves the ledger only on a confirmed removal (_confirmed_gone), in the critical section that clears its diagnostics pointer. Removal is always by id, never by name (which could reach a foreign container).RemoteRuntimehandle is an HTTP client, not evidence of container ownership;_has_cleanupdoes count a live handle as a barrier (it blocks re-entry until cleanup), butclose()gets one bounded attempt (_CLOSEWAIT) and the handle is dropped afterward whatever happens -- a hung or failing close cannot barricade the deployment forever. Apptainer server processes get SIGTERM then SIGKILL with bounded waits, references cleared only on confirmed exit; a failed cleanup keeps the process and log references for the nextstop(). Every teardown record -- cleanup,stop(), everything__del__reaches -- goes through one helper that swallows a raising sink, so a torn-down logger at interpreter shutdown cannot abort a sweep and strand a container; start-only records keep the plain logger and stay loud.commandis a user template that may place it anywhere) and only while it is still alive. Logged exception summaries go through_why; rendered chains and all container output through_redact. Nothing carrying the raw argv or a raw cause reaches a caller's own logger --_runtime_execrunscheck=Falseand rebuilds its error outside theexcept(so no__context__leaks), and the retry loop raises an unchained, redacted summary; the loguru sinks rundiagnose=False. It covers apptainer too, whosePopen.argshold the token. Tokens are registered last in_begin_attempt(after the announce, so a start that dies earlier strands nothing) and released once_has_cleanup()is false.LOCAL_*limits fail fast withValueError. Accepted: a cancelleddocker runkeeps pressuring the daemon until its 600s CLI timeout, and astop()landing on one waits for it; a container that neither cidfile nor owner label ever names must be reclaimed by hand from the ERROR record.LocalDeployment.start'smax_retriesstays atmain's 5 -- the valueAgentEnv.startpasses -- so the retry tests exercise the same retry contractAgentEnvrelies on (they do not run the training path). Target matrix: Linux x86_64 + Docker on the rollout host; macOS is development-only.Checklist Before Submitting
pre-commit install && pre-commit run --all-files --show-diff-on-failure --color=always