[Test] 샌드박스가 띄운 monitor subscriber 데몬(reconcile.sh --subscribe --idle-timeout 0)이 회수되지 않음 — 고아 프로세스 202개 누적 #19

Closed
opened 2026-09-13 06:18:04 +00:00 by Godopu · 1 comment
Owner

증상

전체 테스트 스위트를 돌릴 때마다 pytest 샌드박스 워크스페이스에서 띄운 monitor subscriber 데몬이 종료되지 않고 남는다. 2026-09-13 15:05 KST 기준 아래 프로세스가 202개 살아 있었다 (수동 종료함).

bash /private/var/folders/.../pytest-of-godopu16/pytest-29/test_comp_create_herdr_session2/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0
bash /private/var/folders/.../pytest-of-godopu16/pytest-143/test_e2e_scenario1_standard_li0/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0
... (총 202개)
  • pytest basetemp 는 최근 3개(pytest-151~153)만 남기고 삭제되는데, pytest-29·60·83·112·143 등 이미 삭제된 샌드박스를 가리키는 데몬이 그대로 살아 있었다.
  • SIGTERM 으로는 죽지 않았고 SIGKILL 로만 종료됐다.
  • 누수 발생 테스트(프로세스 수 기준 상위): test_stop_session_codex_sends_* 30, test_onboarding_job_stays_in_s* 25, test_onboarding_job_follows_wo* 25, test_comp_create_herdr_session* 20, test_create_session_full 10, test_create_does_not_inherit_a* 10, test_comp_opencode_create_and_* 10, test_comp_create_sqlite_tables 10, test_comp_create_herdr_workspa* 10, test_comp_codex_create_and_rec* 10, test_e2e_scenario1/2/4_* 각 8~9, test_integration_stop_purge_co* 8, test_integration_create_option* 8.

원인

  1. multi-agent-mux-create/scripts/create_session.sh:527 이 세션 생성 성공 경로에서 start_watchdog 를 호출하고, 이는 lib_py.delegation.daemon.ensure_subscriber_daemon → spawn() 으로 bash reconcile.sh --subscribe --idle-timeout 0 을 start_new_session=True + SIGHUP 무시로 띄운다 (daemon.py:52-70). 부모(테스트 프로세스)가 끝나도 살아남도록 설계된 데몬이다.
  2. 중복 방지는 reconcile.sh:92-101 의 .mam/monitor.lock 워크스페이스 단위 flock 뿐이다. 샌드박스마다 워크스페이스가 다르므로 create 를 호출하는 테스트 하나당 데몬이 하나씩 새로 뜬다.
  3. tests/conftest.py 의 mam_sandbox(15행) 등 픽스처에 데몬을 회수하는 finalizer 가 없고, mock_herdr·mock_agents 는 herdr·에이전트 CLI 만 가짜로 바꿀 뿐 subscriber 스폰 경로는 실제 코드가 그대로 돈다.
  4. --idle-timeout 0 이라 스스로 끝나지 않고(reconcile.sh:88, 278), 워크스페이스 디렉터리가 삭제되어도 감지하지 않는다.
  5. 샌드박스는 MAM_ENV_FILE 을 존재하지 않는 경로로 돌려 .mam.env 를 차단하므로(conftest.py:53-54), 누수된 데몬은 모두 공개 HiveMQ 브로커의 기본값으로 와일드카드 구독을 유지한다. 브로커 연결 202개가 무의미하게 열려 있었던 셈이다.

#15(레지스트리 누수)와 같은 계열의 테스트 격리 문제이며, #15 수정 시 이 경로는 다루지 않았다.

제안

  1. 테스트 격리 (필수): mam_sandbox 에 finalizer 를 추가해, 샌드박스 경로를 cmdline 에 가진 reconcile.sh --subscribe 프로세스와 자식(python subscriber)을 SIGKILL 로 회수한다. 또는 스폰을 옵트아웃하는 환경변수(예: MAM_NO_SUBSCRIBER_DAEMON=1)를 daemon.spawn 에 두고 샌드박스가 기본으로 켜되, 데몬 동작 자체를 검증하는 테스트(test_issue5_phase1_delegation.py:231 등)만 끈다.
  2. 데몬 자기 종료 (권장): subscriber 메인 루프(reconcile.sh:278)가 주기적으로 workspace_root 또는 .mam/monitor.lock 의 존재를 확인하고 사라지면 종료한다. 운영에서도 워크스페이스를 지운 뒤 남는 데몬을 막는다. SIGTERM 핸들러도 추가해 정상 종료되게 한다.
  3. 회귀 가드: #15 의 레지스트리 카운트 가드처럼 세션 스코프 finalizer 에서 스위트 시작 전후의 reconcile.sh --subscribe 프로세스 수를 비교해 증가 시 실패시킨다.
  4. 문서: monitor SKILL.md 에 "데몬은 워크스페이스 단위로 1개, --idle-timeout 0 은 워크스페이스 삭제 시에만 종료" 계약을 명시한다.

재현

pytest -q tests/test_tier2_component.py -k "test_comp_create_herdr_session"
pgrep -fl 'pytest-of-.*reconcile.sh --subscribe'   # 테스트 종료 후에도 프로세스가 남는다

우선순위: Medium-High — 전체 스위트 1회당 약 30개 데몬이 누적되고, 각각 공개 브로커 연결을 유지한다.

## 증상 전체 테스트 스위트를 돌릴 때마다 pytest 샌드박스 워크스페이스에서 띄운 monitor subscriber 데몬이 종료되지 않고 남는다. 2026-09-13 15:05 KST 기준 아래 프로세스가 **202개** 살아 있었다 (수동 종료함). ``` bash /private/var/folders/.../pytest-of-godopu16/pytest-29/test_comp_create_herdr_session2/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0 bash /private/var/folders/.../pytest-of-godopu16/pytest-143/test_e2e_scenario1_standard_li0/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0 ... (총 202개) ``` - pytest basetemp 는 최근 3개(pytest-151~153)만 남기고 삭제되는데, pytest-29·60·83·112·143 등 **이미 삭제된 샌드박스**를 가리키는 데몬이 그대로 살아 있었다. - `SIGTERM` 으로는 죽지 않았고 `SIGKILL` 로만 종료됐다. - 누수 발생 테스트(프로세스 수 기준 상위): `test_stop_session_codex_sends_*` 30, `test_onboarding_job_stays_in_s*` 25, `test_onboarding_job_follows_wo*` 25, `test_comp_create_herdr_session*` 20, `test_create_session_full` 10, `test_create_does_not_inherit_a*` 10, `test_comp_opencode_create_and_*` 10, `test_comp_create_sqlite_tables` 10, `test_comp_create_herdr_workspa*` 10, `test_comp_codex_create_and_rec*` 10, `test_e2e_scenario1/2/4_*` 각 8~9, `test_integration_stop_purge_co*` 8, `test_integration_create_option*` 8. ## 원인 1. `multi-agent-mux-create/scripts/create_session.sh:527` 이 세션 생성 성공 경로에서 `start_watchdog` 를 호출하고, 이는 `lib_py.delegation.daemon.ensure_subscriber_daemon` → `spawn()` 으로 `bash reconcile.sh --subscribe --idle-timeout 0` 을 `start_new_session=True` + `SIGHUP` 무시로 띄운다 (`daemon.py:52-70`). 부모(테스트 프로세스)가 끝나도 살아남도록 설계된 데몬이다. 2. 중복 방지는 `reconcile.sh:92-101` 의 `.mam/monitor.lock` **워크스페이스 단위** flock 뿐이다. 샌드박스마다 워크스페이스가 다르므로 create 를 호출하는 테스트 하나당 데몬이 하나씩 새로 뜬다. 3. `tests/conftest.py` 의 `mam_sandbox`(15행) 등 픽스처에 데몬을 회수하는 finalizer 가 없고, `mock_herdr`·`mock_agents` 는 herdr·에이전트 CLI 만 가짜로 바꿀 뿐 subscriber 스폰 경로는 실제 코드가 그대로 돈다. 4. `--idle-timeout 0` 이라 스스로 끝나지 않고(`reconcile.sh:88, 278`), 워크스페이스 디렉터리가 삭제되어도 감지하지 않는다. 5. 샌드박스는 `MAM_ENV_FILE` 을 존재하지 않는 경로로 돌려 `.mam.env` 를 차단하므로(`conftest.py:53-54`), 누수된 데몬은 모두 **공개 HiveMQ 브로커**의 기본값으로 와일드카드 구독을 유지한다. 브로커 연결 202개가 무의미하게 열려 있었던 셈이다. #15(레지스트리 누수)와 같은 계열의 테스트 격리 문제이며, #15 수정 시 이 경로는 다루지 않았다. ## 제안 1. **테스트 격리 (필수)**: `mam_sandbox` 에 finalizer 를 추가해, 샌드박스 경로를 cmdline 에 가진 `reconcile.sh --subscribe` 프로세스와 자식(python subscriber)을 `SIGKILL` 로 회수한다. 또는 스폰을 옵트아웃하는 환경변수(예: `MAM_NO_SUBSCRIBER_DAEMON=1`)를 `daemon.spawn` 에 두고 샌드박스가 기본으로 켜되, 데몬 동작 자체를 검증하는 테스트(`test_issue5_phase1_delegation.py:231` 등)만 끈다. 2. **데몬 자기 종료 (권장)**: subscriber 메인 루프(`reconcile.sh:278`)가 주기적으로 `workspace_root` 또는 `.mam/monitor.lock` 의 존재를 확인하고 사라지면 종료한다. 운영에서도 워크스페이스를 지운 뒤 남는 데몬을 막는다. `SIGTERM` 핸들러도 추가해 정상 종료되게 한다. 3. **회귀 가드**: #15 의 레지스트리 카운트 가드처럼 세션 스코프 finalizer 에서 스위트 시작 전후의 `reconcile.sh --subscribe` 프로세스 수를 비교해 증가 시 실패시킨다. 4. 문서: monitor SKILL.md 에 "데몬은 워크스페이스 단위로 1개, `--idle-timeout 0` 은 워크스페이스 삭제 시에만 종료" 계약을 명시한다. ## 재현 ``` pytest -q tests/test_tier2_component.py -k "test_comp_create_herdr_session" pgrep -fl 'pytest-of-.*reconcile.sh --subscribe' # 테스트 종료 후에도 프로세스가 남는다 ``` 우선순위: Medium-High — 전체 스위트 1회당 약 30개 데몬이 누적되고, 각각 공개 브로커 연결을 유지한다.
Author
Owner

main 139bdd4 에 병합되어 해결되었습니다 (v4.4.5, 브랜치 fix/issue-19).

  • 옵트아웃: MAM_NO_SUBSCRIBER_DAEMON=1(리터럴 1만)일 때 start_watchdog/ensure_subscriber_daemon 이 데몬을 새로 띄우지 않습니다. 이미 도는 데몬의 PID 는 그대로 보고합니다. 테스트 샌드박스(mam_sandbox)가 기본으로 켭니다.
  • 데몬 자기 종료: reconcile.sh --subscribe 가 워크스페이스 디렉터리 소실 또는 .mam/monitor.lock 의 (st_dev, st_ino) 교체를 감지하면 정리 후 rc 0 으로 끝납니다. 폴링 폴백은 존재만 확인합니다(문서화).
  • SIGTERM/SIGINT 즉시 전달: python 구독자를 백그라운드 자식으로 두고 bash 가 wait 하며, 트랩이 자식에 TERM 을 전달하고 rc 143 으로 끝납니다. 핸들러는 connect 이전에 설치되어 핸드셰이크 중 시그널도 정리 경로를 탑니다.
  • 샌드박스 회수·회귀 가드: mam_sandbox finalizer 가 argv 형태(bash|sh <샌드박스>/…/reconcile.sh --subscribe, 공백 경로 포함)로 데몬과 자식을 회수하고, 세션 스코프 가드가 basetemp 아래 잔존 데몬을 회수한 뒤 실패시킵니다.
  • 테스트: tests/test_issue19_subscriber_daemon_lifecycle.py 15건. 전체 스위트 875 통과, 스위트 후 샌드박스 데몬 0건.
  • 리뷰: claude·agy·grok·codex 4인 [VERDICT: PASS] (3회차, 134fe97).
main `139bdd4` 에 병합되어 해결되었습니다 (v4.4.5, 브랜치 `fix/issue-19`). - **옵트아웃**: `MAM_NO_SUBSCRIBER_DAEMON=1`(리터럴 `1`만)일 때 `start_watchdog`/`ensure_subscriber_daemon` 이 데몬을 새로 띄우지 않습니다. 이미 도는 데몬의 PID 는 그대로 보고합니다. 테스트 샌드박스(`mam_sandbox`)가 기본으로 켭니다. - **데몬 자기 종료**: `reconcile.sh --subscribe` 가 워크스페이스 디렉터리 소실 또는 `.mam/monitor.lock` 의 `(st_dev, st_ino)` 교체를 감지하면 정리 후 rc 0 으로 끝납니다. 폴링 폴백은 존재만 확인합니다(문서화). - **SIGTERM/SIGINT 즉시 전달**: python 구독자를 백그라운드 자식으로 두고 bash 가 `wait` 하며, 트랩이 자식에 TERM 을 전달하고 rc 143 으로 끝납니다. 핸들러는 connect 이전에 설치되어 핸드셰이크 중 시그널도 정리 경로를 탑니다. - **샌드박스 회수·회귀 가드**: `mam_sandbox` finalizer 가 argv 형태(`bash|sh <샌드박스>/…/reconcile.sh --subscribe`, 공백 경로 포함)로 데몬과 자식을 회수하고, 세션 스코프 가드가 basetemp 아래 잔존 데몬을 회수한 뒤 실패시킵니다. - 테스트: `tests/test_issue19_subscriber_daemon_lifecycle.py` 15건. 전체 스위트 875 통과, 스위트 후 샌드박스 데몬 0건. - 리뷰: claude·agy·grok·codex 4인 `[VERDICT: PASS]` (3회차, `134fe97`).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#19