반복적인 IPC 오류 해결, 봇 오류 해결, 인증 오류 해결, 서버 자원 할당 오류 해결, 코드 리팩토링

This commit is contained in:
2026-02-14 18:03:13 +09:00
parent 4fd0aa91bc
commit 9dbf6e6791
15 changed files with 1452 additions and 847 deletions

View File

@@ -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)