refactor(docker): use ExitStack for launch teardown
Replace the manual state-dict + per-resource branching teardown in DockerBottleBackend.launch with an ExitStack: each resource registers its own cleanup callback at the moment it's created, and stack.close() unwinds in LIFO order. The previous form had to hand-coordinate four nullable slots and re-check existence for the container; ExitStack encodes the same semantics declaratively.
This commit is contained in:
@@ -14,7 +14,7 @@ import dataclasses
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import ExitStack, contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, Sequence
|
from typing import Iterator, Sequence
|
||||||
|
|
||||||
@@ -44,6 +44,15 @@ from .provision import ssh as _ssh
|
|||||||
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_remove_container(name: str) -> None:
|
||||||
|
if docker_mod.container_exists(name):
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "rm", "-f", name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DockerBottleBackend(BottleBackend):
|
class DockerBottleBackend(BottleBackend):
|
||||||
"""Docker backend implementation. Selected by CLAUDE_BOTTLE_BACKEND
|
"""Docker backend implementation. Selected by CLAUDE_BOTTLE_BACKEND
|
||||||
(default)."""
|
(default)."""
|
||||||
@@ -162,31 +171,11 @@ class DockerBottleBackend(BottleBackend):
|
|||||||
f"got {type(plan).__name__}"
|
f"got {type(plan).__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
state: dict[str, str] = {
|
stack = ExitStack()
|
||||||
"container": "",
|
|
||||||
"pipelock": "",
|
|
||||||
"internal_network": "",
|
|
||||||
"egress_network": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
def teardown() -> None:
|
def teardown() -> None:
|
||||||
try:
|
try:
|
||||||
if state["container"] and docker_mod.container_exists(state["container"]):
|
stack.close()
|
||||||
subprocess.run(
|
|
||||||
["docker", "rm", "-f", state["container"]],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
state["container"] = ""
|
|
||||||
if state["pipelock"]:
|
|
||||||
self._proxy.stop(state["pipelock"])
|
|
||||||
state["pipelock"] = ""
|
|
||||||
if state["internal_network"]:
|
|
||||||
network_mod.network_remove(state["internal_network"])
|
|
||||||
state["internal_network"] = ""
|
|
||||||
if state["egress_network"]:
|
|
||||||
network_mod.network_remove(state["egress_network"])
|
|
||||||
state["egress_network"] = ""
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
# Teardown must not raise; swallow so the caller's
|
# Teardown must not raise; swallow so the caller's
|
||||||
# __exit__ path can still propagate the original error.
|
# __exit__ path can still propagate the original error.
|
||||||
@@ -199,22 +188,26 @@ class DockerBottleBackend(BottleBackend):
|
|||||||
plan.derived_image, plan.image, plan.spec.user_cwd
|
plan.derived_image, plan.image, plan.spec.user_cwd
|
||||||
)
|
)
|
||||||
|
|
||||||
state["internal_network"] = network_mod.network_create_internal(plan.slug)
|
internal_network = network_mod.network_create_internal(plan.slug)
|
||||||
state["egress_network"] = network_mod.network_create_egress(plan.slug)
|
stack.callback(network_mod.network_remove, internal_network)
|
||||||
|
|
||||||
|
egress_network = network_mod.network_create_egress(plan.slug)
|
||||||
|
stack.callback(network_mod.network_remove, egress_network)
|
||||||
|
|
||||||
proxy_plan = dataclasses.replace(
|
proxy_plan = dataclasses.replace(
|
||||||
plan.proxy_plan,
|
plan.proxy_plan,
|
||||||
internal_network=state["internal_network"],
|
internal_network=internal_network,
|
||||||
egress_network=state["egress_network"],
|
egress_network=egress_network,
|
||||||
)
|
)
|
||||||
state["pipelock"] = self._proxy.start(proxy_plan)
|
pipelock_name = self._proxy.start(proxy_plan)
|
||||||
|
stack.callback(self._proxy.stop, pipelock_name)
|
||||||
|
|
||||||
container = self._run_agent_container(plan, state["internal_network"])
|
container = self._run_agent_container(plan, internal_network)
|
||||||
state["container"] = container
|
stack.callback(_force_remove_container, container)
|
||||||
|
|
||||||
prompt_path = self.provision(plan, container)
|
prompt_path = self.provision(plan, container)
|
||||||
|
|
||||||
bottle = DockerBottle(container, teardown, prompt_path)
|
yield DockerBottle(container, teardown, prompt_path)
|
||||||
yield bottle
|
|
||||||
finally:
|
finally:
|
||||||
teardown()
|
teardown()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user