py-alaska 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_alaska/SmBlock.py +263 -0
- py_alaska/__init__.py +63 -0
- py_alaska/div_logo.png +0 -0
- py_alaska/gconfig.py +1241 -0
- py_alaska/imi_camera.py +391 -0
- py_alaska/tab_camera.py +730 -0
- py_alaska/task_manager.py +661 -0
- py_alaska/task_monitor.py +1533 -0
- py_alaska/task_performance.py +550 -0
- py_alaska/task_signal.py +238 -0
- py_alaska-0.1.0.dist-info/METADATA +263 -0
- py_alaska-0.1.0.dist-info/RECORD +15 -0
- py_alaska-0.1.0.dist-info/WHEEL +5 -0
- py_alaska-0.1.0.dist-info/licenses/LICENSE +21 -0
- py_alaska-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"""
|
|
2
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
║ ALASK v2.0 Project ║
|
|
4
|
+
║ Task Manager - Multiprocess/Thread Management System ║
|
|
5
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
6
|
+
║ Version : 2.0.0 ║
|
|
7
|
+
║ Date : 2026-01-30 ║
|
|
8
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
9
|
+
║ Classes: ║
|
|
10
|
+
║ - TaskClass : Decorator for task class registration ║
|
|
11
|
+
║ - TaskInfo : Dataclass holding task metadata and IPC resources ║
|
|
12
|
+
║ - TaskManager : Main manager for task lifecycle and monitoring ║
|
|
13
|
+
║ - TaskWorker : Worker wrapper executing task loop and RMI handler ║
|
|
14
|
+
║ - RmiClient : Client proxy for remote method invocation ║
|
|
15
|
+
║ - _RmiProxy : Lazy proxy for transparent remote variable access ║
|
|
16
|
+
║ ║
|
|
17
|
+
║ Functions: ║
|
|
18
|
+
║ - _check_port_in_use : Check if a port is already bound ║
|
|
19
|
+
║ - _check_singleton : Ensure single instance via port binding ║
|
|
20
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
from typing import Any, Dict, Optional
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
import threading
|
|
27
|
+
import multiprocessing
|
|
28
|
+
import time
|
|
29
|
+
import socket
|
|
30
|
+
import traceback
|
|
31
|
+
import queue
|
|
32
|
+
import atexit
|
|
33
|
+
import signal
|
|
34
|
+
|
|
35
|
+
from .task_performance import TimingRecorder, PerformanceCollector
|
|
36
|
+
|
|
37
|
+
_task_registry: Dict[str, type] = {}
|
|
38
|
+
_MISSING = object() # sentinel for getattr
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_port_in_use(port: int) -> bool:
|
|
42
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
43
|
+
try:
|
|
44
|
+
s.bind(('0.0.0.0', port))
|
|
45
|
+
return False
|
|
46
|
+
except OSError:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check_singleton(port: int) -> bool:
|
|
51
|
+
if _check_port_in_use(port):
|
|
52
|
+
print(f"\n{'='*60}\n[ERROR] 포트 {port} 사용 중 - 다른 인스턴스 실행 중\n{'='*60}\n")
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TaskClass:
|
|
58
|
+
__slots__ = ('name', 'mode', 'restart', 'restart_delay')
|
|
59
|
+
|
|
60
|
+
def __init__(self, name: str, mode: str = "thread", restart: bool = True, restart_delay: float = 3.0):
|
|
61
|
+
self.name, self.mode, self.restart, self.restart_delay = name, mode, restart, restart_delay
|
|
62
|
+
|
|
63
|
+
def __call__(self, cls):
|
|
64
|
+
_task_registry[self.name] = cls
|
|
65
|
+
cls._tc_name, cls._tc_mode = self.name, self.mode
|
|
66
|
+
cls._tc_restart, cls._tc_delay = self.restart, self.restart_delay
|
|
67
|
+
return cls
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def get(name: str) -> Optional[type]: return _task_registry.get(name)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def attr(name: str, key: str, default=None):
|
|
74
|
+
c = _task_registry.get(name)
|
|
75
|
+
return getattr(c, key, default) if c else default
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class TaskInfo:
|
|
80
|
+
task_class_name: str
|
|
81
|
+
task_id: str = ""
|
|
82
|
+
mode: str = "thread"
|
|
83
|
+
job_class: Optional[type] = None
|
|
84
|
+
injection: Dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
auto_restart: bool = True
|
|
86
|
+
restart_delay: float = 3.0
|
|
87
|
+
request_q: Any = None
|
|
88
|
+
response_q: Any = None
|
|
89
|
+
stop_flag: Any = None
|
|
90
|
+
rmi_rx: Any = None
|
|
91
|
+
rmi_tx: Any = None
|
|
92
|
+
rmi_fail: Any = None
|
|
93
|
+
rmi_time: Any = None
|
|
94
|
+
rmi_time_min: Any = None
|
|
95
|
+
rmi_time_max: Any = None
|
|
96
|
+
restart_cnt: Any = None
|
|
97
|
+
rmi_methods: Any = None # Manager.dict() for per-method call counts
|
|
98
|
+
rmi_timing: Any = None # Manager.dict() for per-method timing (10-window: [ipc_times], [func_times])
|
|
99
|
+
|
|
100
|
+
def __post_init__(self):
|
|
101
|
+
self.task_id = self.task_id or self.task_class_name
|
|
102
|
+
if not self.job_class:
|
|
103
|
+
n = self.task_class_name
|
|
104
|
+
self.job_class = TaskClass.get(n)
|
|
105
|
+
self.mode = TaskClass.attr(n, '_tc_mode', 'thread')
|
|
106
|
+
self.auto_restart = TaskClass.attr(n, '_tc_restart', True)
|
|
107
|
+
self.restart_delay = TaskClass.attr(n, '_tc_delay', 3.0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _RmiProxy:
|
|
111
|
+
__slots__ = ('_client', '_name', '_value', '_resolved')
|
|
112
|
+
|
|
113
|
+
def __init__(self, client: 'RmiClient', name: str):
|
|
114
|
+
# 단일 할당으로 최적화
|
|
115
|
+
sa = object.__setattr__
|
|
116
|
+
sa(self, '_client', client)
|
|
117
|
+
sa(self, '_name', name)
|
|
118
|
+
sa(self, '_value', None)
|
|
119
|
+
sa(self, '_resolved', False)
|
|
120
|
+
|
|
121
|
+
def _resolve(self):
|
|
122
|
+
if not self._resolved:
|
|
123
|
+
sa = object.__setattr__
|
|
124
|
+
sa(self, '_value', self._client._call('__getvar__', self._name))
|
|
125
|
+
sa(self, '_resolved', True)
|
|
126
|
+
return self._value
|
|
127
|
+
|
|
128
|
+
def __call__(self, *a, **kw): return self._client._call(self._name, *a, **kw)
|
|
129
|
+
|
|
130
|
+
# 비교 연산자
|
|
131
|
+
def __eq__(self, o): return self._resolve() == o
|
|
132
|
+
def __ne__(self, o): return self._resolve() != o
|
|
133
|
+
def __lt__(self, o): return self._resolve() < o
|
|
134
|
+
def __le__(self, o): return self._resolve() <= o
|
|
135
|
+
def __gt__(self, o): return self._resolve() > o
|
|
136
|
+
def __ge__(self, o): return self._resolve() >= o
|
|
137
|
+
|
|
138
|
+
# 산술 연산자
|
|
139
|
+
def __add__(self, o): return self._resolve() + o
|
|
140
|
+
def __radd__(self, o): return o + self._resolve()
|
|
141
|
+
def __sub__(self, o): return self._resolve() - o
|
|
142
|
+
def __rsub__(self, o): return o - self._resolve()
|
|
143
|
+
def __mul__(self, o): return self._resolve() * o
|
|
144
|
+
def __rmul__(self, o): return o * self._resolve()
|
|
145
|
+
def __truediv__(self, o): return self._resolve() / o
|
|
146
|
+
def __floordiv__(self, o): return self._resolve() // o
|
|
147
|
+
def __mod__(self, o): return self._resolve() % o
|
|
148
|
+
|
|
149
|
+
# 단항/변환
|
|
150
|
+
def __neg__(self): return -self._resolve()
|
|
151
|
+
def __pos__(self): return +self._resolve()
|
|
152
|
+
def __abs__(self): return abs(self._resolve())
|
|
153
|
+
def __int__(self): return int(self._resolve())
|
|
154
|
+
def __float__(self): return float(self._resolve())
|
|
155
|
+
def __str__(self): return str(self._resolve())
|
|
156
|
+
def __bool__(self): return bool(self._resolve())
|
|
157
|
+
def __repr__(self): return f"_RmiProxy({self._name})"
|
|
158
|
+
|
|
159
|
+
# 컨테이너
|
|
160
|
+
def __len__(self): return len(self._resolve())
|
|
161
|
+
def __getitem__(self, k): return self._resolve()[k]
|
|
162
|
+
def __contains__(self, i): return i in self._resolve()
|
|
163
|
+
def __iter__(self): return iter(self._resolve())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RmiClient:
|
|
167
|
+
__slots__ = ('_id', '_req_q', '_res_q', '_timeout', '_tx_cnt', '_time_cnt', '_time_min', '_time_max')
|
|
168
|
+
|
|
169
|
+
def __init__(self, target_id: str, request_q, response_q, timeout: float = 3.0,
|
|
170
|
+
tx_counter=None, time_counter=None, time_min=None, time_max=None):
|
|
171
|
+
sa = object.__setattr__
|
|
172
|
+
sa(self, '_id', target_id)
|
|
173
|
+
sa(self, '_req_q', request_q)
|
|
174
|
+
sa(self, '_res_q', response_q)
|
|
175
|
+
sa(self, '_timeout', timeout)
|
|
176
|
+
sa(self, '_tx_cnt', tx_counter)
|
|
177
|
+
sa(self, '_time_cnt', time_counter)
|
|
178
|
+
sa(self, '_time_min', time_min)
|
|
179
|
+
sa(self, '_time_max', time_max)
|
|
180
|
+
|
|
181
|
+
def _call(self, m: str, *a, **kw):
|
|
182
|
+
tx = self._tx_cnt
|
|
183
|
+
if tx: tx.value += 1
|
|
184
|
+
t0 = time.time()
|
|
185
|
+
self._req_q.put({'m': m, 'a': a, 'kw': kw, 'rq': self._res_q, 't0': t0})
|
|
186
|
+
try:
|
|
187
|
+
r = self._res_q.get(timeout=self._timeout)
|
|
188
|
+
elapsed = (time.time() - t0) * 1000
|
|
189
|
+
tc, tmin, tmax = self._time_cnt, self._time_min, self._time_max
|
|
190
|
+
if tc: tc.value += elapsed
|
|
191
|
+
if tmin and elapsed < tmin.value: tmin.value = elapsed
|
|
192
|
+
if tmax and elapsed > tmax.value: tmax.value = elapsed
|
|
193
|
+
if r.get('e'):
|
|
194
|
+
err_msg = r['e']
|
|
195
|
+
if r.get('trace'):
|
|
196
|
+
err_msg += f"\n--- Remote Traceback ---\n{r['trace']}"
|
|
197
|
+
raise Exception(err_msg)
|
|
198
|
+
return r.get('r')
|
|
199
|
+
except queue.Empty:
|
|
200
|
+
raise Exception(f"RmiClient({self._id}).{m}: Timeout after {self._timeout}s")
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise Exception(f"RmiClient({self._id}).{m}: {e}") from e
|
|
203
|
+
|
|
204
|
+
def __getattr__(self, m: str):
|
|
205
|
+
if m[0] == '_': raise AttributeError(m)
|
|
206
|
+
return _RmiProxy(self, m)
|
|
207
|
+
|
|
208
|
+
def __setattr__(self, m: str, value):
|
|
209
|
+
if m[0] == '_':
|
|
210
|
+
object.__setattr__(self, m, value)
|
|
211
|
+
else:
|
|
212
|
+
self._call('__setvar__', m, value)
|
|
213
|
+
|
|
214
|
+
def __repr__(self): return f"RmiClient({self._id})"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TaskManager:
|
|
218
|
+
__slots__ = ('_tasks', '_workers', '_mgr', '_monitor_port', '_monitor', '_gconfig', '_exit_hook', '_measurement', '_performance')
|
|
219
|
+
|
|
220
|
+
def __init__(self, gconfig):
|
|
221
|
+
"""Initialize TaskManager with GConfig instance.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
gconfig: GConfig instance with task_config section containing:
|
|
225
|
+
- task definitions: "task_id/class_name": {config...}
|
|
226
|
+
- _monitor (optional): {"port": int, "exit_hook": bool}
|
|
227
|
+
- port: Required for monitor to start
|
|
228
|
+
- exit_hook: If True, stop_all() is called on process exit
|
|
229
|
+
"""
|
|
230
|
+
self._tasks: Dict[str, TaskInfo] = {}
|
|
231
|
+
self._workers: Dict[str, Any] = {}
|
|
232
|
+
self._mgr = multiprocessing.Manager()
|
|
233
|
+
self._gconfig = gconfig
|
|
234
|
+
self._monitor = None
|
|
235
|
+
self._exit_hook = False
|
|
236
|
+
|
|
237
|
+
# Extract task_config from gconfig
|
|
238
|
+
config = gconfig.data_get("task_config", {})
|
|
239
|
+
|
|
240
|
+
# Handle _monitor settings
|
|
241
|
+
monitor_cfg = config.get("_monitor", {})
|
|
242
|
+
self._monitor_port = monitor_cfg.get("port") if monitor_cfg else None
|
|
243
|
+
self._exit_hook = monitor_cfg.get("exit_hook", False) if monitor_cfg else False
|
|
244
|
+
# RMI method measurement (shared across processes)
|
|
245
|
+
measurement_default = monitor_cfg.get("measurement", False) if monitor_cfg else False
|
|
246
|
+
self._measurement = self._mgr.Value('b', measurement_default)
|
|
247
|
+
|
|
248
|
+
mgr = self._mgr
|
|
249
|
+
|
|
250
|
+
for key, cfg in config.items():
|
|
251
|
+
# Skip keys starting with '_' (like _monitor)
|
|
252
|
+
if key.startswith("_"):
|
|
253
|
+
continue
|
|
254
|
+
tid, tcn = key.split("/", 1) if "/" in key else (key, cfg.get("@task") or cfg.get("@job") or key)
|
|
255
|
+
inj = {k: v for k, v in cfg.items() if k[0] != "@"}
|
|
256
|
+
ti = TaskInfo(task_class_name=tcn, task_id=tid, injection=inj)
|
|
257
|
+
ti.request_q, ti.response_q = mgr.Queue(), mgr.Queue()
|
|
258
|
+
ti.stop_flag = mgr.Value('b', False)
|
|
259
|
+
ti.rmi_rx, ti.rmi_tx, ti.rmi_fail = mgr.Value('i', 0), mgr.Value('i', 0), mgr.Value('i', 0)
|
|
260
|
+
ti.rmi_time, ti.rmi_time_min, ti.rmi_time_max = mgr.Value('d', 0.0), mgr.Value('d', float('inf')), mgr.Value('d', 0.0)
|
|
261
|
+
ti.restart_cnt = mgr.Value('i', 0)
|
|
262
|
+
ti.rmi_methods = mgr.dict() # Per-method call counts
|
|
263
|
+
ti.rmi_timing = mgr.dict() # Per-method timing (10-window)
|
|
264
|
+
self._tasks[tid] = ti
|
|
265
|
+
|
|
266
|
+
# PerformanceCollector 생성 (성능 통계 수집용)
|
|
267
|
+
self._performance = PerformanceCollector(self._tasks, self._workers, self._measurement)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def performance(self) -> PerformanceCollector:
|
|
271
|
+
"""성능 데이터 수집기 접근"""
|
|
272
|
+
return self._performance
|
|
273
|
+
|
|
274
|
+
def start_all(self, exit_on_duplicate: bool = True):
|
|
275
|
+
if self._monitor_port and not _check_singleton(self._monitor_port):
|
|
276
|
+
raise SystemExit(1) if exit_on_duplicate else RuntimeError(f"Port {self._monitor_port} in use")
|
|
277
|
+
|
|
278
|
+
self._print()
|
|
279
|
+
|
|
280
|
+
# SmBlock 생성 (_smblock 설정이 있으면)
|
|
281
|
+
SmBlockHandler.creation(self._gconfig, self._mgr, self._tasks)
|
|
282
|
+
|
|
283
|
+
for tid, ti in self._tasks.items():
|
|
284
|
+
if tid not in self._workers:
|
|
285
|
+
w = TaskWorker(ti, self._tasks, measurement=self._measurement)
|
|
286
|
+
h = multiprocessing.Process(target=w.run, daemon=True) if ti.mode == 'process' else threading.Thread(target=w.run, daemon=True)
|
|
287
|
+
h.start()
|
|
288
|
+
self._workers[tid] = h
|
|
289
|
+
print(f"[Manager] {tid} started ({ti.mode})")
|
|
290
|
+
|
|
291
|
+
if self._monitor_port and not self._monitor:
|
|
292
|
+
from .task_monitor import TaskMonitor
|
|
293
|
+
self._monitor = TaskMonitor(self, self._monitor_port, gconfig=self._gconfig)
|
|
294
|
+
self._monitor.start()
|
|
295
|
+
|
|
296
|
+
# Register exit handler if exit_hook is enabled
|
|
297
|
+
if self._exit_hook:
|
|
298
|
+
atexit.register(self.stop_all)
|
|
299
|
+
# signal.signal can only be called from main thread
|
|
300
|
+
if threading.current_thread() is threading.main_thread():
|
|
301
|
+
signal.signal(signal.SIGINT, lambda s, f: self.stop_all())
|
|
302
|
+
print("[Manager] exit_hook registered (atexit + SIGINT)")
|
|
303
|
+
else:
|
|
304
|
+
print("[Manager] exit_hook registered (atexit only, non-main thread)")
|
|
305
|
+
|
|
306
|
+
def stop_all(self, skip_monitor: bool = False):
|
|
307
|
+
print(f"[Manager:stop_all] called, tasks={list(self._tasks.keys())}, skip_monitor={skip_monitor}")
|
|
308
|
+
if self._monitor and not skip_monitor:
|
|
309
|
+
print(f"[Manager:stop_all] stopping monitor")
|
|
310
|
+
self._monitor.stop()
|
|
311
|
+
self._monitor = None
|
|
312
|
+
for tid, ti in self._tasks.items():
|
|
313
|
+
print(f"[Manager:stop_all] setting stop_flag for {tid}")
|
|
314
|
+
ti.stop_flag.value = True
|
|
315
|
+
for tid, w in self._workers.items():
|
|
316
|
+
print(f"[Manager:stop_all] joining {tid}, alive={w.is_alive()}")
|
|
317
|
+
w.join(timeout=4.0) # RMI timeout(3s) + margin
|
|
318
|
+
if w.is_alive():
|
|
319
|
+
print(f"[Manager:stop_all] {tid} still alive after timeout, terminating...")
|
|
320
|
+
if hasattr(w, 'terminate'):
|
|
321
|
+
w.terminate()
|
|
322
|
+
print(f"[Manager:stop_all] {tid} stopped")
|
|
323
|
+
self._workers.clear()
|
|
324
|
+
|
|
325
|
+
# SmBlock 정리
|
|
326
|
+
SmBlockHandler.close_all()
|
|
327
|
+
print(f"[Manager:stop_all] completed")
|
|
328
|
+
|
|
329
|
+
def clear_stats(self):
|
|
330
|
+
inf = float('inf')
|
|
331
|
+
for ti in self._tasks.values():
|
|
332
|
+
ti.rmi_rx.value = ti.rmi_tx.value = ti.rmi_fail.value = 0
|
|
333
|
+
ti.rmi_time.value = ti.rmi_time_max.value = 0.0
|
|
334
|
+
ti.rmi_time_min.value = inf
|
|
335
|
+
ti.restart_cnt.value = 0
|
|
336
|
+
ti.rmi_methods.clear() # Clear per-method counts
|
|
337
|
+
ti.rmi_timing.clear() # Clear per-method timing
|
|
338
|
+
|
|
339
|
+
def get_status(self) -> Dict[str, Dict[str, Any]]:
|
|
340
|
+
result = {}
|
|
341
|
+
inf = float('inf')
|
|
342
|
+
for tid, ti in self._tasks.items():
|
|
343
|
+
w = self._workers.get(tid)
|
|
344
|
+
tx = ti.rmi_tx.value
|
|
345
|
+
tt = ti.rmi_time.value
|
|
346
|
+
tmin = ti.rmi_time_min.value
|
|
347
|
+
result[tid] = {
|
|
348
|
+
'class': ti.task_class_name, 'mode': ti.mode,
|
|
349
|
+
'alive': w.is_alive() if w else False,
|
|
350
|
+
'rmi_rx': ti.rmi_rx.value, 'rmi_tx': tx, 'rmi_fail': ti.rmi_fail.value,
|
|
351
|
+
'rmi_avg': tt / tx if tx else 0.0,
|
|
352
|
+
'rmi_min': 0.0 if tmin == inf else tmin,
|
|
353
|
+
'rmi_max': ti.rmi_time_max.value,
|
|
354
|
+
'rxq_size': ti.request_q.qsize(),
|
|
355
|
+
'txq_size': ti.response_q.qsize(),
|
|
356
|
+
'restart': ti.restart_cnt.value,
|
|
357
|
+
'rmi_methods': dict(ti.rmi_methods), # Per-method call counts
|
|
358
|
+
}
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def _print(self):
|
|
362
|
+
print("\n" + "=" * 60)
|
|
363
|
+
print(f"{'ID':<15} {'Class':<12} {'Mode':<8} {'Job':<15}")
|
|
364
|
+
print("-" * 60)
|
|
365
|
+
for tid, ti in self._tasks.items():
|
|
366
|
+
print(f"{tid:<15} {ti.task_class_name:<12} {ti.mode:<8} {ti.job_class.__name__ if ti.job_class else '-':<15}")
|
|
367
|
+
print("=" * 60 + "\n")
|
|
368
|
+
|
|
369
|
+
def __enter__(self): self.start_all(); return self
|
|
370
|
+
def __exit__(self, *_): self.stop_all(); return False
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class TaskWorker:
|
|
374
|
+
__slots__ = ('ti', 'tasks', 'job', '_run', '_th', '_measurement', '_timing_recorder')
|
|
375
|
+
|
|
376
|
+
def __init__(self, ti: TaskInfo, tasks: Dict[str, TaskInfo], measurement=None):
|
|
377
|
+
self.ti, self.tasks, self.job, self._run, self._th = ti, tasks, None, True, None
|
|
378
|
+
self._measurement = measurement # Shared measurement flag
|
|
379
|
+
self._timing_recorder = TimingRecorder(ti.rmi_methods, ti.rmi_timing)
|
|
380
|
+
|
|
381
|
+
def run(self):
|
|
382
|
+
ti = self.ti
|
|
383
|
+
if not ti.job_class:
|
|
384
|
+
print(f"[W:{ti.task_id}] no job_class"); return
|
|
385
|
+
self.job = ti.job_class()
|
|
386
|
+
self.job.task = self
|
|
387
|
+
self._th = threading.Thread(target=self._rmi_handler, daemon=True)
|
|
388
|
+
self._th.start()
|
|
389
|
+
self._inject()
|
|
390
|
+
print(f"[W:{ti.task_id}] started")
|
|
391
|
+
self._loop()
|
|
392
|
+
|
|
393
|
+
def _rmi_handler(self):
|
|
394
|
+
ti = self.ti
|
|
395
|
+
q, job = ti.request_q, self.job
|
|
396
|
+
stop = ti.stop_flag
|
|
397
|
+
rx, fail = ti.rmi_rx, ti.rmi_fail
|
|
398
|
+
recorder = self._timing_recorder
|
|
399
|
+
|
|
400
|
+
while self._run and not stop.value:
|
|
401
|
+
try:
|
|
402
|
+
req = q.get(timeout=0.5)
|
|
403
|
+
except queue.Empty:
|
|
404
|
+
# Flush on idle
|
|
405
|
+
if self._measurement and self._measurement.value:
|
|
406
|
+
recorder.flush()
|
|
407
|
+
continue
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print(f"[W:{ti.task_id}] Queue error at {__file__}: {e}")
|
|
410
|
+
continue
|
|
411
|
+
if stop.value: break
|
|
412
|
+
|
|
413
|
+
t_recv = time.time() # Time when request was received
|
|
414
|
+
rx.value += 1
|
|
415
|
+
m, a, kw, rq = req['m'], req.get('a', ()), req.get('kw', {}), req.get('rq')
|
|
416
|
+
t0 = req.get('t0') # Time when request was sent (for IPC measurement)
|
|
417
|
+
|
|
418
|
+
# 측정 여부 결정 (내부 메서드 '_' 제외)
|
|
419
|
+
measure = m[0] != '_' and self._measurement and self._measurement.value
|
|
420
|
+
|
|
421
|
+
t_func_start = time.time() # Start of function execution
|
|
422
|
+
try:
|
|
423
|
+
# 빠른 경로: 일반 메서드 호출 (가장 빈번)
|
|
424
|
+
if m[0] != '_':
|
|
425
|
+
fn = getattr(job, m, _MISSING)
|
|
426
|
+
if fn is _MISSING:
|
|
427
|
+
res = {'e': f"'{m}' not found"}
|
|
428
|
+
fail.value += 1
|
|
429
|
+
else:
|
|
430
|
+
res = {'r': fn(*a, **kw) if callable(fn) else fn}
|
|
431
|
+
# 특수 메서드 디스패치
|
|
432
|
+
elif m == '__getvar__':
|
|
433
|
+
val = getattr(job, a[0], _MISSING)
|
|
434
|
+
if val is _MISSING:
|
|
435
|
+
res = {'e': f"'{a[0]}' not found"}
|
|
436
|
+
fail.value += 1
|
|
437
|
+
else:
|
|
438
|
+
res = {'r': val() if callable(val) else val}
|
|
439
|
+
elif m == '__setvar__':
|
|
440
|
+
setattr(job, a[0], a[1])
|
|
441
|
+
res = {'r': True}
|
|
442
|
+
elif m == '__set_config__':
|
|
443
|
+
print(f"[W:{ti.task_id}:RMI] __set_config__ key={a[0]}, value={a[1]}, type={type(a[1])}")
|
|
444
|
+
before_val = getattr(job, a[0], _MISSING)
|
|
445
|
+
print(f"[W:{ti.task_id}:RMI] before setattr: {a[0]}={before_val}")
|
|
446
|
+
setattr(job, a[0], a[1])
|
|
447
|
+
after_val = getattr(job, a[0], _MISSING)
|
|
448
|
+
print(f"[W:{ti.task_id}:RMI] after setattr: {a[0]}={after_val}")
|
|
449
|
+
res = {'r': True}
|
|
450
|
+
elif m == '__get_config__':
|
|
451
|
+
res = {'r': {k: getattr(job, k, None) for k in (a[0] if a else [])}}
|
|
452
|
+
else:
|
|
453
|
+
res = {'e': f"Unknown method '{m}'"}
|
|
454
|
+
fail.value += 1
|
|
455
|
+
except Exception as e:
|
|
456
|
+
res = {'e': str(e), 'trace': traceback.format_exc()}
|
|
457
|
+
fail.value += 1
|
|
458
|
+
print(f"[W:{ti.task_id}] RMI error in '{m}': {e}")
|
|
459
|
+
|
|
460
|
+
# Record timing (IPC + FUNC) via TimingRecorder
|
|
461
|
+
if measure:
|
|
462
|
+
t_func_end = time.time()
|
|
463
|
+
if t0:
|
|
464
|
+
ipc_ms = (t_recv - t0) * 1000 # IPC time in ms
|
|
465
|
+
func_ms = (t_func_end - t_func_start) * 1000 # FUNC time in ms
|
|
466
|
+
recorder.record(m, ipc_ms, func_ms)
|
|
467
|
+
else:
|
|
468
|
+
recorder.record_count_only(m)
|
|
469
|
+
|
|
470
|
+
if rq:
|
|
471
|
+
try:
|
|
472
|
+
rq.put(res)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
print(f"[W:{ti.task_id}] Response queue error: {e}")
|
|
475
|
+
|
|
476
|
+
# Final flush on exit
|
|
477
|
+
recorder.flush()
|
|
478
|
+
|
|
479
|
+
def _inject(self):
|
|
480
|
+
job, tasks, ti = self.job, self.tasks, self.ti
|
|
481
|
+
SmBlockHandler.injection(self, tasks)
|
|
482
|
+
for k, v in ti.injection.items():
|
|
483
|
+
# smblock 설정은 SmBlockHandler에서 처리됨
|
|
484
|
+
if isinstance(v, dict) and "_smblock" in v:
|
|
485
|
+
continue
|
|
486
|
+
if isinstance(v, str) and v[:5] == "task:":
|
|
487
|
+
t = tasks.get(v[5:])
|
|
488
|
+
if t:
|
|
489
|
+
setattr(job, k, RmiClient(
|
|
490
|
+
v[5:], t.request_q, ti.response_q,
|
|
491
|
+
tx_counter=ti.rmi_tx, time_counter=ti.rmi_time,
|
|
492
|
+
time_min=ti.rmi_time_min, time_max=ti.rmi_time_max
|
|
493
|
+
))
|
|
494
|
+
else:
|
|
495
|
+
setattr(job, k, v)
|
|
496
|
+
|
|
497
|
+
def _loop(self):
|
|
498
|
+
ti, job = self.ti, self.job
|
|
499
|
+
task_loop = getattr(job, 'task_loop', None)
|
|
500
|
+
if not task_loop: return
|
|
501
|
+
|
|
502
|
+
while self._run:
|
|
503
|
+
try:
|
|
504
|
+
task_loop()
|
|
505
|
+
break
|
|
506
|
+
except Exception as e:
|
|
507
|
+
print(f"[W:{ti.task_id}] {e}")
|
|
508
|
+
if not ti.auto_restart: break
|
|
509
|
+
ti.restart_cnt.value += 1
|
|
510
|
+
time.sleep(ti.restart_delay)
|
|
511
|
+
|
|
512
|
+
def should_stop(self) -> bool: return not self._run or self.ti.stop_flag.value
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def name(self) -> str: return self.ti.task_id
|
|
516
|
+
|
|
517
|
+
class SmBlockHandler:
|
|
518
|
+
"""SmBlock 공유메모리 블록 할당/주입 핸들러.
|
|
519
|
+
|
|
520
|
+
Config format example:
|
|
521
|
+
task_config:
|
|
522
|
+
_smblock:
|
|
523
|
+
image_pool: {shape: [480, 640, 3], maxsize: 100}
|
|
524
|
+
task_id/class_name:
|
|
525
|
+
camera: "smblock:image_pool" # Inject SmBlock reference
|
|
526
|
+
|
|
527
|
+
동작 원리:
|
|
528
|
+
1. creation(): _smblock 설정을 파싱하여 SmBlock 생성
|
|
529
|
+
- "smblock:name" 문자열을 dict로 변환하여 injection에 저장
|
|
530
|
+
- dict에는 name, shape, maxsize, lock 포함 (프로세스 간 공유)
|
|
531
|
+
2. injection(): 워커에서 dict를 읽어 SmBlock 인스턴스 생성/연결
|
|
532
|
+
- thread 모드: _pools에서 직접 참조
|
|
533
|
+
- process 모드: injection dict의 설정으로 새로 attach
|
|
534
|
+
"""
|
|
535
|
+
_pools: Dict[str, Any] = {} # name -> SmBlock instance (메인 프로세스용)
|
|
536
|
+
|
|
537
|
+
@classmethod
|
|
538
|
+
def creation(cls, gconfig, mgr, tasks: Dict[str, TaskInfo]) -> Dict[str, Any]:
|
|
539
|
+
"""_smblock 설정에서 SmBlock 인스턴스 생성 및 injection 설정 변환.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
gconfig: GConfig 인스턴스 (task_config._smblock 설정 포함)
|
|
543
|
+
mgr: multiprocessing.Manager 인스턴스 (Lock 생성용)
|
|
544
|
+
tasks: TaskInfo 딕셔너리 (injection 설정 변환용)
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Dict[str, SmBlock]: name -> SmBlock 인스턴스 매핑
|
|
548
|
+
"""
|
|
549
|
+
from .SmBlock import SmBlock
|
|
550
|
+
|
|
551
|
+
config = gconfig.data_get("task_config", {})
|
|
552
|
+
smblock_cfg = config.get("_smblock", {})
|
|
553
|
+
|
|
554
|
+
if not smblock_cfg:
|
|
555
|
+
return cls._pools
|
|
556
|
+
|
|
557
|
+
# 1. SmBlock 생성 및 설정 저장
|
|
558
|
+
smblock_info = {} # name -> {shape, maxsize, lock}
|
|
559
|
+
for name, cfg in smblock_cfg.items():
|
|
560
|
+
if name in cls._pools:
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
shape = cfg.get("shape")
|
|
564
|
+
maxsize = cfg.get("maxsize", 100)
|
|
565
|
+
|
|
566
|
+
if not shape:
|
|
567
|
+
print(f"[SmBlockHandler] '{name}': shape 필수 설정 누락")
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
shape = tuple(shape) if isinstance(shape, list) else shape
|
|
571
|
+
lock = mgr.Lock()
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
pool = SmBlock(name, shape, maxsize, create=True, lock=lock)
|
|
575
|
+
cls._pools[name] = pool
|
|
576
|
+
smblock_info[name] = {"name": name, "shape": shape, "maxsize": maxsize, "lock": lock}
|
|
577
|
+
print(f"[SmBlockHandler] SmBlock '{name}' created: shape={shape}, maxsize={maxsize}")
|
|
578
|
+
except FileExistsError:
|
|
579
|
+
# 기존 공유 메모리가 남아있는 경우 - 정리 후 재생성
|
|
580
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
581
|
+
try:
|
|
582
|
+
old_shm = SharedMemory(name=name, create=False)
|
|
583
|
+
old_shm.close()
|
|
584
|
+
old_shm.unlink()
|
|
585
|
+
print(f"[SmBlockHandler] SmBlock '{name}' old memory cleaned up")
|
|
586
|
+
except Exception:
|
|
587
|
+
pass
|
|
588
|
+
# 새로 생성
|
|
589
|
+
pool = SmBlock(name, shape, maxsize, create=True, lock=lock)
|
|
590
|
+
cls._pools[name] = pool
|
|
591
|
+
smblock_info[name] = {"name": name, "shape": shape, "maxsize": maxsize, "lock": lock}
|
|
592
|
+
print(f"[SmBlockHandler] SmBlock '{name}' recreated: shape={shape}, maxsize={maxsize}")
|
|
593
|
+
except Exception as e:
|
|
594
|
+
print(f"[SmBlockHandler] SmBlock '{name}' creation failed: {e}")
|
|
595
|
+
|
|
596
|
+
# 2. 각 task의 injection에서 "smblock:xxx" 문자열을 dict로 변환
|
|
597
|
+
for tid, ti in tasks.items():
|
|
598
|
+
for k, v in list(ti.injection.items()):
|
|
599
|
+
if isinstance(v, str) and v.startswith("smblock:"):
|
|
600
|
+
pool_name = v[8:]
|
|
601
|
+
if pool_name in smblock_info:
|
|
602
|
+
# 문자열 -> dict 변환 (Manager Lock 포함)
|
|
603
|
+
ti.injection[k] = {
|
|
604
|
+
"_smblock": pool_name,
|
|
605
|
+
**smblock_info[pool_name]
|
|
606
|
+
}
|
|
607
|
+
print(f"[SmBlockHandler] {tid}.{k}: injection converted to dict")
|
|
608
|
+
else:
|
|
609
|
+
# 정의되지 않은 smblock 참조 - 오류 발생
|
|
610
|
+
available = list(smblock_info.keys()) if smblock_info else "(none)"
|
|
611
|
+
raise ValueError(
|
|
612
|
+
f"[SmBlockHandler] {tid}.{k}: SmBlock '{pool_name}' not defined in _smblock. "
|
|
613
|
+
f"Available: {available}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
return cls._pools
|
|
617
|
+
|
|
618
|
+
@classmethod
|
|
619
|
+
def injection(cls, worker: 'TaskWorker', _tasks: Dict[str, TaskInfo]):
|
|
620
|
+
"""smblock injection dict를 SmBlock 인스턴스로 변환하여 job에 주입.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
worker: TaskWorker 인스턴스
|
|
624
|
+
_tasks: 전체 태스크 정보 딕셔너리 (미사용, API 일관성 유지)
|
|
625
|
+
"""
|
|
626
|
+
from .SmBlock import SmBlock
|
|
627
|
+
|
|
628
|
+
job = worker.job
|
|
629
|
+
ti = worker.ti
|
|
630
|
+
|
|
631
|
+
for k, v in ti.injection.items():
|
|
632
|
+
# dict 형태의 smblock 설정 처리
|
|
633
|
+
if isinstance(v, dict) and "_smblock" in v:
|
|
634
|
+
pool_name = v["_smblock"]
|
|
635
|
+
shape = tuple(v["shape"])
|
|
636
|
+
maxsize = v["maxsize"]
|
|
637
|
+
lock = v["lock"] # Manager Lock (프로세스 간 공유됨)
|
|
638
|
+
|
|
639
|
+
# thread 모드: 메인 프로세스의 _pools에서 참조
|
|
640
|
+
if pool_name in cls._pools:
|
|
641
|
+
setattr(job, k, cls._pools[pool_name])
|
|
642
|
+
print(f"[SmBlockHandler] {ti.task_id}.{k} <- SmBlock '{pool_name}' (direct)")
|
|
643
|
+
else:
|
|
644
|
+
# process 모드: 새로 attach
|
|
645
|
+
try:
|
|
646
|
+
pool = SmBlock(pool_name, shape, maxsize, create=False, lock=lock)
|
|
647
|
+
setattr(job, k, pool)
|
|
648
|
+
print(f"[SmBlockHandler] {ti.task_id}.{k} <- SmBlock '{pool_name}' (attached)")
|
|
649
|
+
except Exception as e:
|
|
650
|
+
print(f"[SmBlockHandler] {ti.task_id}.{k}: SmBlock attach failed: {e}")
|
|
651
|
+
|
|
652
|
+
@classmethod
|
|
653
|
+
def close_all(cls):
|
|
654
|
+
"""모든 SmBlock 인스턴스 정리 (메인 프로세스에서만 호출)."""
|
|
655
|
+
for name, pool in cls._pools.items():
|
|
656
|
+
try:
|
|
657
|
+
pool.close()
|
|
658
|
+
print(f"[SmBlockHandler] SmBlock '{name}' closed")
|
|
659
|
+
except Exception as e:
|
|
660
|
+
print(f"[SmBlockHandler] SmBlock '{name}' close failed: {e}")
|
|
661
|
+
cls._pools.clear()
|