Skip to content

Commit 1517faa

Browse files
takemi-ohamaclaude
andauthored
fix(compose): generate_scaled_compose に init: true を注入してゾンビ reap (#28) (#32)
devbase コンテナの PID 1 は entrypoint の `tail -f /dev/null` で orphan を reap しないため、`nohup ... & disown` で起動したプロセスがゾンビ化して蓄積する。 特に cross-review の monitor.py がゾンビを「実行中」と誤判定し hard timeout まで 待たされる二次被害が出る。 generate_scaled_compose() の dev / non-dev 両サービス生成ループで `setdefault('init', True)` を注入し、docker が tini を PID 1 に挿入するように する。devbase up は常にこの生成ファイル単独で compose up するため scale=1 でも 網羅される。setdefault のため明示的な `init: false` は尊重する。 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1ca1c85 commit 1517faa

3 files changed

Lines changed: 81 additions & 0 deletions

File tree

lib/devbase/volume/compose.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ def generate_scaled_compose(
203203
if service_name != dev_service_name:
204204
copied = _deep_copy(service_config)
205205
_rewrite_depends_on(copied, dev_service_name, scale)
206+
# Insert tini as PID 1 so orphaned children are reaped (no zombies).
207+
# setdefault keeps an explicit `init: false` if the project set one.
208+
copied.setdefault('init', True)
206209
scaled_config['services'][service_name] = copied
207210

208211
# Generate a service for each instance
@@ -216,6 +219,10 @@ def generate_scaled_compose(
216219
# Update container name
217220
service['container_name'] = f"${{COMPOSE_PROJECT_NAME}}-{dev_service_name}-{i}"
218221

222+
# Insert tini as PID 1 so orphaned children are reaped (no zombies).
223+
# setdefault keeps an explicit `init: false` if the project set one.
224+
service.setdefault('init', True)
225+
219226
# Remove environment section (use env_file instead to avoid exposing secrets)
220227
if 'environment' in service:
221228
del service['environment']

tests/volume/__init__.py

Whitespace-only changes.

tests/volume/test_compose.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""compose.py: generate_scaled_compose() の `init: true` 注入挙動 (issue #28)
2+
3+
devbase コンテナの PID 1 は entrypoint の `tail -f /dev/null` であり orphan を reap しない。
4+
docker の `init: true` で tini を PID 1 に挿入しゾンビ蓄積を防ぐため、生成される全サービスに
5+
`init` を注入する。ユーザーが明示した `init: false` は setdefault で尊重する。
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
12+
import yaml
13+
import pytest
14+
15+
from devbase.volume import compose
16+
17+
18+
@pytest.fixture
19+
def in_tmp_cwd(tmp_path, monkeypatch):
20+
"""生成物 (.docker-compose.scale.yml) が散らからないよう CWD を tmp に移す。"""
21+
monkeypatch.chdir(tmp_path)
22+
# DEV_SERVICE_NAME が外部環境に左右されないよう既定に固定
23+
monkeypatch.delenv("DEV_SERVICE_NAME", raising=False)
24+
return tmp_path
25+
26+
27+
def _write_compose(tmp_path, services: dict) -> None:
28+
(tmp_path / "compose.yml").write_text(
29+
yaml.safe_dump({"services": services}, sort_keys=False),
30+
encoding="utf-8",
31+
)
32+
33+
34+
def _load_scaled(tmp_path) -> dict:
35+
return yaml.safe_load((tmp_path / ".docker-compose.scale.yml").read_text())
36+
37+
38+
def test_dev_and_non_dev_services_get_init_true(in_tmp_cwd):
39+
"""dev インスタンスと non-dev サービスの双方に init: true が注入される。"""
40+
_write_compose(in_tmp_cwd, {
41+
"dev": {"image": "dev:latest"},
42+
"mysql": {"image": "mysql:8"},
43+
})
44+
45+
compose.generate_scaled_compose(scale=1, project_name="proj")
46+
scaled = _load_scaled(in_tmp_cwd)["services"]
47+
48+
assert scaled["dev-1"]["init"] is True
49+
assert scaled["mysql"]["init"] is True
50+
51+
52+
def test_init_injected_for_every_scaled_instance(in_tmp_cwd):
53+
"""scale>1 でも各 dev-i 全てに init: true が付く。"""
54+
_write_compose(in_tmp_cwd, {"dev": {"image": "dev:latest"}})
55+
56+
compose.generate_scaled_compose(scale=3, project_name="proj")
57+
scaled = _load_scaled(in_tmp_cwd)["services"]
58+
59+
for i in (1, 2, 3):
60+
assert scaled[f"dev-{i}"]["init"] is True
61+
62+
63+
def test_explicit_init_false_is_preserved(in_tmp_cwd):
64+
"""明示的な init: false は setdefault により上書きされない。"""
65+
_write_compose(in_tmp_cwd, {
66+
"dev": {"image": "dev:latest", "init": False},
67+
"mysql": {"image": "mysql:8", "init": False},
68+
})
69+
70+
compose.generate_scaled_compose(scale=1, project_name="proj")
71+
scaled = _load_scaled(in_tmp_cwd)["services"]
72+
73+
assert scaled["dev-1"]["init"] is False
74+
assert scaled["mysql"]["init"] is False

0 commit comments

Comments
 (0)