Files
pifu/resident_worker.py
2026-09-11 13:41:40 +08:00

136 lines
4.6 KiB
Python

"""Spawned model processes, reused across tasks and replaceable after a crash."""
import logging
import multiprocessing
import queue
import time
logger = logging.getLogger(__name__)
def analysis_process(connection, model_path, workers):
try:
from wanzheng import SafeYOLO, analyze_directory
model = SafeYOLO(model_path)
connection.send({'ready': True})
while True:
task = connection.recv()
if task is None:
return
result = analyze_directory(task['input_dir'], task['task_id'], model,
workers, task.get('api_url'))
connection.send(result)
except EOFError:
pass
except BaseException as exc:
logger.exception('常驻分析进程异常')
try:
connection.send({'error': str(exc) or type(exc).__name__})
except (OSError, EOFError):
pass
finally:
connection.close()
class ResidentPool:
def __init__(self, model_path, concurrency=1, workers=4, api_url=None,
startup_timeout=180, task_timeout=3600, target=analysis_process):
if min(concurrency, workers, startup_timeout, task_timeout) <= 0:
raise ValueError('并发、线程数和超时必须大于 0')
self.model_path = str(model_path)
self.concurrency = concurrency
self.workers = workers
self.api_url = api_url
self.startup_timeout = startup_timeout
self.task_timeout = task_timeout
self.target = target
self.context = multiprocessing.get_context('spawn')
self.slots = queue.Queue()
def receive(self, slot, timeout, stop_event=None):
connection, process = slot
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if stop_event is not None and stop_event.is_set():
raise RuntimeError('服务停止,任务中断,请重新提交')
if connection.poll(0.1):
try:
return connection.recv()
except (EOFError, OSError) as exc:
raise RuntimeError('常驻分析进程连接中断') from exc
if not process.is_alive():
raise RuntimeError(f'常驻分析进程退出:{process.exitcode}')
raise RuntimeError('常驻分析进程等待超时')
@staticmethod
def dispose(slot):
if slot is None:
return
connection, process = slot
connection.close()
if process.is_alive():
process.terminate()
process.join(timeout=5)
if process.is_alive():
process.kill()
process.join()
process.close()
def spawn(self, stop_event=None):
parent, child = self.context.Pipe()
process = self.context.Process(target=self.target,
args=(child, self.model_path, self.workers))
try:
process.start()
except BaseException:
parent.close()
child.close()
process.close()
raise
child.close()
slot = parent, process
try:
message = self.receive(slot, self.startup_timeout, stop_event)
if not message.get('ready'):
raise RuntimeError(message.get('error', '模型预热失败'))
return slot
except BaseException:
self.dispose(slot)
raise
def start(self):
try:
for _ in range(self.concurrency):
self.slots.put(self.spawn())
except BaseException:
self.stop()
raise
def stop(self):
# TaskQueue joins its worker threads before closing idle slots.
while True:
try:
self.dispose(self.slots.get_nowait())
except queue.Empty:
break
def __call__(self, task, stop_event):
slot = self.slots.get()
try:
if stop_event.is_set():
raise RuntimeError('服务停止,任务中断,请重新提交')
if slot is None or not slot[1].is_alive():
self.dispose(slot)
slot = None
slot = self.spawn(stop_event)
slot[0].send(dict(task, api_url=self.api_url))
result = self.receive(slot, self.task_timeout, stop_event)
except BaseException:
self.dispose(slot)
slot = None
raise
finally:
self.slots.put(slot)
if result.get('error'):
raise RuntimeError(result['error'])
return result