refactor: web-ai V1 assets → signal_v1/ (graduation prep)
Atomic mv of root V1 assets (main_server.py + modules/ + data/ + tests/ + entry scripts + docs + logs) into signal_v1/ subdirectory. load_dotenv() updated to load web-ai/.env explicitly via Path. Adds web-ai/CLAUDE.md (workspace guide) and web-ai/start.bat (signal_v1 entry wrapper). Prepares for signal_v2/ Phase 2. Tests: signal_v1/tests/unit baseline preserved (no regression). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
183
signal_v1/modules/utils/process_tracker.py
Normal file
183
signal_v1/modules/utils/process_tracker.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
프로세스 생명주기 관리
|
||||
- 메모리 기반 PID 관리 (pids.txt 폐기)
|
||||
- Watchdog 헬스체크
|
||||
- 자동 재시작 (최대 3회)
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
|
||||
from modules.config import Config
|
||||
|
||||
# EOD 마커 파일: 오늘 장 마감 후 봇이 기록, Watchdog가 재시작 여부 결정에 사용
|
||||
class ProcessTracker:
|
||||
"""메모리 기반 프로세스 추적기"""
|
||||
|
||||
# 클래스 변수: 등록된 프로세스 정보
|
||||
_processes = {} # {name: pid}
|
||||
_lock = threading.Lock()
|
||||
|
||||
# 하위 호환: 기존 pids.txt 정리용
|
||||
FILE_PATH = "pids.txt"
|
||||
|
||||
@staticmethod
|
||||
def register(name):
|
||||
"""현재 프로세스 등록 (메모리 기반)"""
|
||||
pid = os.getpid()
|
||||
with ProcessTracker._lock:
|
||||
ProcessTracker._processes[name] = pid
|
||||
print(f"[Process] Registered: {name} (PID: {pid})")
|
||||
|
||||
@staticmethod
|
||||
def unregister(name):
|
||||
"""프로세스 등록 해제"""
|
||||
with ProcessTracker._lock:
|
||||
ProcessTracker._processes.pop(name, None)
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
"""등록된 모든 프로세스 반환"""
|
||||
with ProcessTracker._lock:
|
||||
return dict(ProcessTracker._processes)
|
||||
|
||||
@staticmethod
|
||||
def check_and_kill_zombies():
|
||||
"""이전 실행의 좀비 프로세스 정리 + stale SharedMemory 정리"""
|
||||
# 1. pids.txt 기반 좀비 정리 (하위 호환)
|
||||
if os.path.exists(ProcessTracker.FILE_PATH):
|
||||
try:
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
|
||||
with open(ProcessTracker.FILE_PATH, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
killed_count = 0
|
||||
for line in lines:
|
||||
if ":" not in line or "Running Processes" in line:
|
||||
continue
|
||||
try:
|
||||
pid = int(line.split(":")[0].strip())
|
||||
if pid == current_pid:
|
||||
continue
|
||||
if psutil.pid_exists(pid):
|
||||
proc = psutil.Process(pid)
|
||||
if "python" in proc.name().lower():
|
||||
print(f"[Process] Killing zombie: PID {pid} ({line.strip()})")
|
||||
proc.kill()
|
||||
killed_count += 1
|
||||
except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
if killed_count > 0:
|
||||
print(f"[Process] Cleaned up {killed_count} zombie processes.")
|
||||
except Exception as e:
|
||||
print(f"[Process] Zombie cleanup failed: {e}")
|
||||
|
||||
# pids.txt 삭제 (더 이상 사용하지 않음)
|
||||
try:
|
||||
os.remove(ProcessTracker.FILE_PATH)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Stale SharedMemory 정리
|
||||
try:
|
||||
shm = SharedMemory(name=Config.SHM_NAME, create=False)
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
print(f"[Process] Cleaned stale SharedMemory: {Config.SHM_NAME}")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
"""등록 정보 초기화"""
|
||||
with ProcessTracker._lock:
|
||||
ProcessTracker._processes.clear()
|
||||
|
||||
|
||||
class ProcessWatchdog:
|
||||
"""자식 프로세스 감시 및 자동 재시작"""
|
||||
|
||||
def __init__(self, shutdown_event=None):
|
||||
self.shutdown_event = shutdown_event
|
||||
self._watched = {} # {name: {process, target, args, restart_count}}
|
||||
self._thread = None
|
||||
self._running = False
|
||||
|
||||
def watch(self, name, process, target, args=()):
|
||||
"""프로세스를 감시 대상에 등록"""
|
||||
self._watched[name] = {
|
||||
'process': process,
|
||||
'target': target,
|
||||
'args': args,
|
||||
'restart_count': 0
|
||||
}
|
||||
|
||||
def start(self):
|
||||
"""Watchdog 스레드 시작"""
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._watchdog_loop, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[Watchdog] Started (interval: {Config.WATCHDOG_INTERVAL}s)")
|
||||
|
||||
def stop(self):
|
||||
"""Watchdog 중지"""
|
||||
self._running = False
|
||||
if self._thread:
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
def get_process(self, name):
|
||||
"""감시 중인 프로세스 반환"""
|
||||
entry = self._watched.get(name)
|
||||
return entry['process'] if entry else None
|
||||
|
||||
def _watchdog_loop(self):
|
||||
"""주기적으로 자식 프로세스 상태 확인"""
|
||||
import multiprocessing
|
||||
|
||||
while self._running:
|
||||
if self.shutdown_event and self.shutdown_event.is_set():
|
||||
break
|
||||
|
||||
for name, entry in list(self._watched.items()):
|
||||
proc = entry['process']
|
||||
|
||||
if proc.is_alive():
|
||||
continue
|
||||
|
||||
# 프로세스가 종료됨
|
||||
exit_code = proc.exitcode
|
||||
restart_count = entry['restart_count']
|
||||
|
||||
if restart_count >= Config.MAX_RESTART_COUNT:
|
||||
print(f"[Watchdog] {name} crashed (exit={exit_code}). "
|
||||
f"Max restarts ({Config.MAX_RESTART_COUNT}) reached. Giving up.")
|
||||
continue
|
||||
|
||||
print(f"[Watchdog] {name} crashed (exit={exit_code}). "
|
||||
f"Restarting... ({restart_count + 1}/{Config.MAX_RESTART_COUNT})")
|
||||
|
||||
try:
|
||||
new_proc = multiprocessing.Process(
|
||||
target=entry['target'],
|
||||
args=entry['args']
|
||||
)
|
||||
new_proc.start()
|
||||
entry['process'] = new_proc
|
||||
entry['restart_count'] = restart_count + 1
|
||||
print(f"[Watchdog] {name} restarted (new PID: {new_proc.pid})")
|
||||
except Exception as e:
|
||||
print(f"[Watchdog] Failed to restart {name}: {e}")
|
||||
|
||||
# 인터벌 대기 (shutdown_event 체크하면서)
|
||||
for _ in range(Config.WATCHDOG_INTERVAL):
|
||||
if not self._running:
|
||||
break
|
||||
if self.shutdown_event and self.shutdown_event.is_set():
|
||||
break
|
||||
time.sleep(1)
|
||||
Reference in New Issue
Block a user