반복적인 IPC 오류 해결, 봇 오류 해결, 인증 오류 해결, 서버 자원 할당 오류 해결, 코드 리팩토링
This commit is contained in:
@@ -1,62 +1,126 @@
|
||||
"""
|
||||
프로세스 간 통신 (IPC) - 파일 기반
|
||||
텔레그램 봇과 메인 봇 간 데이터 공유
|
||||
프로세스 간 통신 (IPC) - Shared Memory 기반
|
||||
텔레그램 봇과 메인 봇 간 데이터 공유 + 양방향 명령 채널
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import struct
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
|
||||
from modules.config import Config
|
||||
|
||||
class BotIPC:
|
||||
"""파일 기반 IPC (Inter-Process Communication)"""
|
||||
|
||||
def __init__(self, ipc_file=None):
|
||||
self.ipc_file = ipc_file if ipc_file else Config.IPC_FILE
|
||||
self.last_update = 0
|
||||
|
||||
|
||||
class SharedIPC:
|
||||
"""Shared Memory + Command Queue 기반 IPC"""
|
||||
|
||||
def __init__(self, lock=None, command_queue=None):
|
||||
self.lock = lock
|
||||
self.command_queue = command_queue
|
||||
self._shm = None
|
||||
self._is_creator = False
|
||||
|
||||
def _ensure_shm(self):
|
||||
"""SharedMemory 블록에 연결 (없으면 생성)"""
|
||||
if self._shm is not None:
|
||||
return self._shm
|
||||
try:
|
||||
self._shm = SharedMemory(name=Config.SHM_NAME, create=False)
|
||||
except FileNotFoundError:
|
||||
self._shm = SharedMemory(name=Config.SHM_NAME, create=True, size=Config.SHM_SIZE)
|
||||
self._is_creator = True
|
||||
# 초기화: 길이 필드를 0으로 설정
|
||||
struct.pack_into('I', self._shm.buf, 0, 0)
|
||||
return self._shm
|
||||
|
||||
def write_status(self, data):
|
||||
"""메인 봇이 상태를 파일에 기록"""
|
||||
"""메인 봇이 상태를 shared memory에 기록"""
|
||||
try:
|
||||
with open(self.ipc_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
'timestamp': time.time(),
|
||||
'data': data
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
shm = self._ensure_shm()
|
||||
payload = json.dumps({
|
||||
'timestamp': time.time(),
|
||||
'data': data
|
||||
}, ensure_ascii=False).encode('utf-8')
|
||||
|
||||
if len(payload) + 4 > Config.SHM_SIZE:
|
||||
print(f"[IPC] Data too large: {len(payload)} bytes")
|
||||
return
|
||||
|
||||
if self.lock:
|
||||
self.lock.acquire()
|
||||
try:
|
||||
# [4바이트 길이][JSON 페이로드]
|
||||
struct.pack_into('I', shm.buf, 0, len(payload))
|
||||
shm.buf[4:4 + len(payload)] = payload
|
||||
finally:
|
||||
if self.lock:
|
||||
self.lock.release()
|
||||
except Exception as e:
|
||||
print(f"⚠️ [IPC] Write failed: {e}")
|
||||
|
||||
print(f"[IPC] Write failed: {e}")
|
||||
|
||||
def read_status(self):
|
||||
"""텔레그램 봇이 상태를 파일에서 읽기"""
|
||||
"""텔레그램 봇이 상태를 shared memory에서 읽기"""
|
||||
try:
|
||||
if not os.path.exists(self.ipc_file):
|
||||
print(f"⚠️ [IPC] File not found: {self.ipc_file}")
|
||||
shm = self._ensure_shm()
|
||||
|
||||
if self.lock:
|
||||
self.lock.acquire()
|
||||
try:
|
||||
length = struct.unpack_from('I', shm.buf, 0)[0]
|
||||
if length == 0 or length > Config.SHM_SIZE - 4:
|
||||
return None
|
||||
raw = bytes(shm.buf[4:4 + length])
|
||||
finally:
|
||||
if self.lock:
|
||||
self.lock.release()
|
||||
|
||||
ipc_data = json.loads(raw.decode('utf-8'))
|
||||
age = time.time() - ipc_data.get('timestamp', 0)
|
||||
|
||||
if age > Config.IPC_STALENESS:
|
||||
print(f"[IPC] Data too old: {age:.1f}s")
|
||||
return None
|
||||
|
||||
with open(self.ipc_file, 'r', encoding='utf-8') as f:
|
||||
ipc_data = json.load(f)
|
||||
|
||||
# 60초 이상 오래된 데이터는 무시 (10초 → 60초로 완화)
|
||||
timestamp = ipc_data.get('timestamp', 0)
|
||||
age = time.time() - timestamp
|
||||
|
||||
if age > 60:
|
||||
print(f"⚠️ [IPC] Data too old: {age:.1f}s")
|
||||
return None
|
||||
|
||||
print(f"✅ [IPC] Data loaded (age: {age:.1f}s)")
|
||||
|
||||
return ipc_data.get('data')
|
||||
except Exception as e:
|
||||
print(f"⚠️ [IPC] Read failed: {e}")
|
||||
print(f"[IPC] Read failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# --- 명령 채널 (텔레그램 → 메인 봇) ---
|
||||
|
||||
def send_command(self, command, **kwargs):
|
||||
"""텔레그램 → 메인 봇 명령 전송"""
|
||||
if self.command_queue:
|
||||
try:
|
||||
self.command_queue.put_nowait({
|
||||
'command': command,
|
||||
'timestamp': time.time(),
|
||||
**kwargs
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[IPC] Command send failed: {e}")
|
||||
return False
|
||||
|
||||
def poll_commands(self):
|
||||
"""메인 봇이 명령 큐를 폴링"""
|
||||
commands = []
|
||||
if self.command_queue:
|
||||
try:
|
||||
while not self.command_queue.empty():
|
||||
cmd = self.command_queue.get_nowait()
|
||||
commands.append(cmd)
|
||||
except Exception:
|
||||
pass
|
||||
return commands
|
||||
|
||||
# --- FakeBot 인스턴스 (호환성 유지) ---
|
||||
|
||||
def get_bot_instance_data(self):
|
||||
"""봇 인스턴스 데이터 가져오기 (호환성 유지)"""
|
||||
"""봇 인스턴스 데이터 가져오기 (텔레그램 봇용)"""
|
||||
status = self.read_status()
|
||||
if not status:
|
||||
return None
|
||||
|
||||
# 가짜 봇 인스턴스 객체 생성 (기존 코드 호환)
|
||||
|
||||
class FakeBotInstance:
|
||||
def __init__(self, data):
|
||||
self.kis = FakeKIS(data.get('balance', {}), data.get('macro_indices', {}))
|
||||
@@ -66,98 +130,76 @@ class BotIPC:
|
||||
self.is_macro_warning_sent = data.get('is_macro_warning', False)
|
||||
self.watchlist_manager = FakeWatchlistManager(data.get('watchlist', {}))
|
||||
self.load_watchlist = lambda: data.get('watchlist', {})
|
||||
|
||||
|
||||
class FakeKIS:
|
||||
def __init__(self, balance_data, macro_indices):
|
||||
self._balance = balance_data if balance_data else {
|
||||
'total_eval': 0,
|
||||
'deposit': 0,
|
||||
'holdings': []
|
||||
'total_eval': 0, 'deposit': 0, 'holdings': []
|
||||
}
|
||||
self._macro_indices = macro_indices if macro_indices else {}
|
||||
|
||||
|
||||
def get_balance(self):
|
||||
return self._balance
|
||||
|
||||
|
||||
def get_current_index(self, ticker):
|
||||
"""지수 조회 - IPC에서 저장된 데이터 반환"""
|
||||
if ticker in self._macro_indices:
|
||||
return self._macro_indices[ticker]
|
||||
# 데이터 없으면 기본값
|
||||
return {
|
||||
'price': 2500.0,
|
||||
'change': 0.0
|
||||
}
|
||||
|
||||
return {'price': 2500.0, 'change': 0.0}
|
||||
|
||||
def get_daily_index_price(self, ticker, period="D"):
|
||||
"""지수 일별 시세 조회 - IPC 모드에서는 더미 데이터 반환"""
|
||||
# MacroAnalyzer의 MSI 계산용
|
||||
# 실제 데이터는 메인 봇에서만 조회 가능
|
||||
# IPC 모드에서는 기본 더미 데이터 반환 (20일치)
|
||||
base_price = 2500.0
|
||||
if ticker in self._macro_indices:
|
||||
base_price = self._macro_indices[ticker].get('price', 2500.0)
|
||||
|
||||
# 20일치 더미 데이터 (약간의 변동)
|
||||
import random
|
||||
prices = []
|
||||
for i in range(20):
|
||||
variation = random.uniform(-0.02, 0.02) # ±2% 변동
|
||||
prices.append(base_price * (1 + variation))
|
||||
return prices
|
||||
|
||||
return [base_price * (1 + random.uniform(-0.02, 0.02)) for _ in range(20)]
|
||||
|
||||
def get_current_price(self, ticker):
|
||||
"""현재가 조회 - IPC 모드에서는 사용 불가"""
|
||||
return None
|
||||
|
||||
|
||||
def get_daily_price(self, ticker, period="D"):
|
||||
"""일별 시세 조회 - IPC 모드에서는 사용 불가"""
|
||||
return []
|
||||
|
||||
|
||||
def get_volume_rank(self, market="0"):
|
||||
"""거래량 순위 조회 - IPC 모드에서는 사용 불가"""
|
||||
return []
|
||||
|
||||
|
||||
def buy_stock(self, ticker, qty):
|
||||
"""매수 주문 - IPC 모드에서는 사용 불가"""
|
||||
return {"success": False, "msg": "IPC mode: buy not available"}
|
||||
|
||||
return {"success": False, "msg": "IPC mode"}
|
||||
|
||||
def sell_stock(self, ticker, qty):
|
||||
"""매도 주문 - IPC 모드에서는 사용 불가"""
|
||||
return {"success": False, "msg": "IPC mode: sell not available"}
|
||||
|
||||
return {"success": False, "msg": "IPC mode"}
|
||||
|
||||
class FakeOllama:
|
||||
def __init__(self, gpu_data):
|
||||
self._gpu = gpu_data if gpu_data else {
|
||||
'name': 'N/A',
|
||||
'temp': 0,
|
||||
'vram_used': 0,
|
||||
'vram_total': 0,
|
||||
'load': 0
|
||||
'name': 'N/A', 'temp': 0, 'vram_used': 0, 'vram_total': 0, 'load': 0
|
||||
}
|
||||
|
||||
|
||||
def get_gpu_status(self):
|
||||
return self._gpu
|
||||
|
||||
|
||||
class FakeThemeManager:
|
||||
def __init__(self, themes_data):
|
||||
self._themes = themes_data if themes_data else {}
|
||||
|
||||
|
||||
def get_themes(self, ticker):
|
||||
return self._themes.get(ticker, [])
|
||||
|
||||
|
||||
class FakeWatchlistManager:
|
||||
def __init__(self, watchlist_data):
|
||||
self._watchlist = watchlist_data if watchlist_data else {}
|
||||
|
||||
|
||||
def update_watchlist_daily(self):
|
||||
return "⚠️ Watchlist update not available in IPC mode"
|
||||
|
||||
return "Watchlist update not available in IPC mode"
|
||||
|
||||
return FakeBotInstance(status)
|
||||
|
||||
def load_watchlist(self):
|
||||
"""Watchlist 로드"""
|
||||
status = self.read_status()
|
||||
if status:
|
||||
return status.get('watchlist', {})
|
||||
return {}
|
||||
|
||||
def cleanup(self):
|
||||
"""리소스 정리"""
|
||||
if self._shm:
|
||||
try:
|
||||
self._shm.close()
|
||||
if self._is_creator:
|
||||
self._shm.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
self._shm = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import psutil
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SystemMonitor:
|
||||
def __init__(self, messenger, ollama_manager):
|
||||
self.messenger = messenger
|
||||
@@ -9,64 +9,51 @@ class SystemMonitor:
|
||||
self.last_health_check = datetime.now()
|
||||
|
||||
def check_health(self):
|
||||
"""시스템 상태 점검 및 알림 (CPU, RAM, GPU) - 5분마다 실행"""
|
||||
"""시스템 상태 점검 및 알림 (CPU, RAM, GPU) - 3분마다 실행"""
|
||||
now = datetime.now()
|
||||
# 5분에 한 번씩만 체크
|
||||
if (now - self.last_health_check).total_seconds() < 300:
|
||||
if (now - self.last_health_check).total_seconds() < 180: # 5분 → 3분
|
||||
return
|
||||
|
||||
self.last_health_check = now
|
||||
alerts = []
|
||||
|
||||
# 1. CPU Check (Double Verify)
|
||||
# 1초 간격으로 측정
|
||||
cpu_usage = psutil.cpu_percent(interval=1)
|
||||
|
||||
# 1. CPU Check (non-blocking 측정)
|
||||
cpu_usage = psutil.cpu_percent(interval=0)
|
||||
|
||||
if cpu_usage > 90:
|
||||
# 일시적인 스파이크일 수 있으므로 3초 후 재측정
|
||||
time.sleep(3)
|
||||
cpu_usage_2nd = psutil.cpu_percent(interval=1)
|
||||
|
||||
if cpu_usage_2nd > 90:
|
||||
# 과부하 시 원인 프로세스 추적
|
||||
top_processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
|
||||
try:
|
||||
# Windows 유휴 프로세스 제외
|
||||
if proc.info['name'] == 'System Idle Process':
|
||||
continue
|
||||
top_processes.append(proc.info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
# CPU 사용률 내림차순 정렬
|
||||
top_processes.sort(key=lambda x: x['cpu_percent'], reverse=True)
|
||||
|
||||
# 상위 프로세스들의 CPU 합계 검증 (측정 오류 필터링)
|
||||
total_top_cpu = sum(p['cpu_percent'] for p in top_processes[:3])
|
||||
if total_top_cpu < 30.0:
|
||||
print(f"⚠️ [Monitor] Ignored CPU Alert: usage={cpu_usage_2nd}% but top3_sum={total_top_cpu}%")
|
||||
else:
|
||||
top_3_str = ""
|
||||
for p in top_processes[:3]:
|
||||
top_3_str += f"\n- {p['name']} ({p['cpu_percent']}%)"
|
||||
|
||||
alerts.append(f"🔥 **[CPU Overload]** Usage: `{cpu_usage_2nd}%`\n**Top Processes:**{top_3_str}")
|
||||
# 검증: 상위 프로세스 CPU 합계 확인
|
||||
top_processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
|
||||
try:
|
||||
if proc.info['name'] == 'System Idle Process':
|
||||
continue
|
||||
top_processes.append(proc.info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
top_processes.sort(key=lambda x: x['cpu_percent'], reverse=True)
|
||||
total_top_cpu = sum(p['cpu_percent'] for p in top_processes[:3])
|
||||
|
||||
if total_top_cpu >= 30.0:
|
||||
top_3_str = ""
|
||||
for p in top_processes[:3]:
|
||||
top_3_str += f"\n- {p['name']} ({p['cpu_percent']}%)"
|
||||
alerts.append(f"[CPU Overload] Usage: {cpu_usage}%\nTop Processes:{top_3_str}")
|
||||
|
||||
# 2. RAM Check
|
||||
ram = psutil.virtual_memory()
|
||||
if ram.percent > 90:
|
||||
alerts.append(f"💾 **[RAM High]** Usage: `{ram.percent}%` (Free: {ram.available / 1024**3:.1f}GB)")
|
||||
alerts.append(f"[RAM High] Usage: {ram.percent}% (Free: {ram.available / 1024**3:.1f}GB)")
|
||||
|
||||
# 3. GPU Check
|
||||
if self.ollama_monitor:
|
||||
gpu_status = self.ollama_monitor.get_gpu_status()
|
||||
temp = gpu_status.get('temp', 0)
|
||||
if temp > 80:
|
||||
alerts.append(f"♨️ **[GPU Overheat]** Temp: `{temp}°C`")
|
||||
alerts.append(f"[GPU Overheat] Temp: {temp}C")
|
||||
|
||||
# 알림 전송
|
||||
if alerts:
|
||||
msg = "⚠️ **[System Health Alert]**\n" + "\n".join(alerts)
|
||||
msg = "[System Health Alert]\n" + "\n".join(alerts)
|
||||
if self.messenger:
|
||||
self.messenger.send_message(msg)
|
||||
|
||||
@@ -1,78 +1,183 @@
|
||||
"""
|
||||
프로세스 생명주기 관리
|
||||
- 메모리 기반 PID 관리 (pids.txt 폐기)
|
||||
- Watchdog 헬스체크
|
||||
- 자동 재시작 (최대 3회)
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
|
||||
from modules.config import Config
|
||||
|
||||
|
||||
class ProcessTracker:
|
||||
"""메모리 기반 프로세스 추적기"""
|
||||
|
||||
# 클래스 변수: 등록된 프로세스 정보
|
||||
_processes = {} # {name: pid}
|
||||
_lock = threading.Lock()
|
||||
|
||||
# 하위 호환: 기존 pids.txt 정리용
|
||||
FILE_PATH = "pids.txt"
|
||||
|
||||
@staticmethod
|
||||
def register(name):
|
||||
"""현재 프로세스의 PID와 이름을 기록"""
|
||||
"""현재 프로세스 등록 (메모리 기반)"""
|
||||
pid = os.getpid()
|
||||
entry = f"{pid}: {name} (Started: {time.strftime('%Y-%m-%d %H:%M:%S')})\n"
|
||||
|
||||
try:
|
||||
# 파일이 없으면 생성, 있으면 추가
|
||||
# 단, main_server 시작 시 초기화하는 것이 좋음
|
||||
with open(ProcessTracker.FILE_PATH, "a", encoding="utf-8") as f:
|
||||
f.write(entry)
|
||||
print(f"📌 Process Registered: {name} (PID: {pid})")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to register process: {e}")
|
||||
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():
|
||||
"""
|
||||
pids.txt에 기록된 이전 프로세스들이 구동 중이라면 강제 종료.
|
||||
서버 시작 시 1회 호출하여 좀비 프로세스를 정리함.
|
||||
"""
|
||||
if not os.path.exists(ProcessTracker.FILE_PATH):
|
||||
return
|
||||
"""이전 실행의 좀비 프로세스 정리 + stale SharedMemory 정리"""
|
||||
# 1. pids.txt 기반 좀비 정리 (하위 호환)
|
||||
if os.path.exists(ProcessTracker.FILE_PATH):
|
||||
try:
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
|
||||
print("🔍 Checking for zombie processes...")
|
||||
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_str = line.split(":")[0].strip()
|
||||
pid = int(pid_str)
|
||||
|
||||
if pid == current_pid:
|
||||
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
|
||||
|
||||
if psutil.pid_exists(pid):
|
||||
proc = psutil.Process(pid)
|
||||
proc_name = proc.name()
|
||||
|
||||
# Python 프로세스만 타겟
|
||||
if "python" in proc_name.lower():
|
||||
print(f"💀 Killing Zombie Process: {pid} ({line.strip()})")
|
||||
proc.kill()
|
||||
killed_count += 1
|
||||
except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
if killed_count > 0:
|
||||
print(f"✅ Cleaned up {killed_count} zombie processes.")
|
||||
# 파일 초기화
|
||||
ProcessTracker.clear()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to kill zombies: {e}")
|
||||
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():
|
||||
"""PID 파일 초기화"""
|
||||
try:
|
||||
with open(ProcessTracker.FILE_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(f"--- Running Processes (Last Update: {time.strftime('%Y-%m-%d %H:%M:%S')}) ---\n")
|
||||
except:
|
||||
pass
|
||||
"""등록 정보 초기화"""
|
||||
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