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