🐛 Fix container stop issue
This commit is contained in:
+51
-19
@@ -11,7 +11,10 @@ Security model:
|
||||
Docker network; the port is not published to the host.
|
||||
* Requires a bearer token (CONTROL_API_TOKEN) shared with the backend.
|
||||
* Never mounts docker.sock; signals the broker over the shared PID
|
||||
namespace (broker is PID 1 in this container's view).
|
||||
namespace. OWNERSHIP IS INVERTED: this sidecar owns the namespace
|
||||
(tini is PID 1) and the broker joins it — so the sidecar survives
|
||||
broker restarts and can orchestrate them (spec section 29). The
|
||||
broker's PID is discovered via /proc, not hardcoded.
|
||||
|
||||
Implementation notes:
|
||||
* Atomic activation writes content to a temp file in the SAME
|
||||
@@ -98,18 +101,47 @@ def _activate(content: str) -> None:
|
||||
raise
|
||||
|
||||
|
||||
def _broker_pid() -> int | None:
|
||||
"""Find the broker process in this PID namespace.
|
||||
|
||||
The namespace is OWNED by this sidecar (compose inversion: the broker
|
||||
joins us via `pid: "service:mosquitto-control"`), so the broker is
|
||||
NOT PID 1 — discover it by scanning /proc for the mosquitto binary.
|
||||
PID 1 here is tini (the sidecar's init).
|
||||
"""
|
||||
try:
|
||||
for entry in os.listdir("/proc"):
|
||||
if not entry.isdigit():
|
||||
continue
|
||||
pid = int(entry)
|
||||
if pid == 1:
|
||||
continue # tini (this sidecar's init), never the broker
|
||||
try:
|
||||
with open(f"/proc/{pid}/comm", encoding="utf-8") as f:
|
||||
if f.read().strip() == "mosquitto":
|
||||
return pid
|
||||
except OSError:
|
||||
continue # process vanished mid-scan
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _signal_reload() -> None:
|
||||
"""Signal the broker (PID 1 in the shared PID namespace) to reload."""
|
||||
"""Signal the broker to reload its configuration."""
|
||||
pid = _broker_pid()
|
||||
if pid is None:
|
||||
raise _Error(503, "broker process not found in the PID namespace")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["mosquitto_signal", "-p", "1", "config-reload"],
|
||||
["mosquitto_signal", "-p", str(pid), "config-reload"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# Fallback if the helper app is unavailable for any reason.
|
||||
os.kill(1, signal.SIGHUP)
|
||||
os.kill(pid, signal.SIGHUP)
|
||||
return
|
||||
if proc.returncode != 0:
|
||||
raise _Error(500, f"mosquitto_signal failed: {proc.stderr.strip()}")
|
||||
@@ -118,25 +150,25 @@ def _signal_reload() -> None:
|
||||
def _restart_broker() -> None:
|
||||
"""Gracefully stop the broker; Docker's restart policy revives it.
|
||||
|
||||
The broker is PID 1 in the shared PID namespace. SIGTERM makes it
|
||||
shut down cleanly (persistence saved); `restart: unless-stopped`
|
||||
on the mosquitto service then starts the container again. This
|
||||
sidecar is a separate container and survives, so it can report
|
||||
status while the broker is down (spec section 29: restart broker
|
||||
where explicitly supported — no Docker socket involved).
|
||||
The broker is a normal process in THIS sidecar's PID namespace (we
|
||||
own it; the broker joined us). SIGTERM makes it shut down cleanly
|
||||
(persistence saved); `restart: unless-stopped` on the mosquitto
|
||||
service then starts the container again. Because the sidecar is the
|
||||
namespace init, it SURVIVES the broker's death and keeps reporting
|
||||
status through the outage (spec section 29 — no Docker socket).
|
||||
"""
|
||||
# The HTTP response MUST be flushed before PID 1 dies, so the kill
|
||||
# happens after the handler returns — orchestrated by the route.
|
||||
os.kill(1, signal.SIGTERM)
|
||||
# The HTTP response MUST be flushed before the broker dies, so the
|
||||
# kill happens after the handler returns — orchestrated by the route.
|
||||
pid = _broker_pid()
|
||||
if pid is None:
|
||||
# Already down (restart in flight): nothing to signal, and the
|
||||
# caller still gets a clean "restarting" response.
|
||||
return
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
|
||||
|
||||
def _broker_pid_alive() -> bool:
|
||||
try:
|
||||
# Broker is PID 1 in the shared PID namespace.
|
||||
with open("/proc/1/comm", encoding="utf-8") as f:
|
||||
return f.read().strip() == "mosquitto"
|
||||
except OSError:
|
||||
return False
|
||||
return _broker_pid() is not None
|
||||
|
||||
|
||||
def _post_broker_health() -> bool:
|
||||
|
||||
Reference in New Issue
Block a user