rainbow-rb-flow-manager 0.0.9.dev5__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.
- rainbow/rb_flow_manager/__init__.py +0 -0
- rainbow/rb_flow_manager/context.py +610 -0
- rainbow/rb_flow_manager/controller/base_controller.py +88 -0
- rainbow/rb_flow_manager/controller/zenoh_controller.py +328 -0
- rainbow/rb_flow_manager/example.py +89 -0
- rainbow/rb_flow_manager/exception.py +45 -0
- rainbow/rb_flow_manager/executor.py +2405 -0
- rainbow/rb_flow_manager/global_resolver.py +20 -0
- rainbow/rb_flow_manager/py.typed +0 -0
- rainbow/rb_flow_manager/schema.py +88 -0
- rainbow/rb_flow_manager/step.py +1629 -0
- rainbow/rb_flow_manager/utils.py +67 -0
- rainbow_rb_flow_manager-0.0.9.dev5.dist-info/METADATA +142 -0
- rainbow_rb_flow_manager-0.0.9.dev5.dist-info/RECORD +16 -0
- rainbow_rb_flow_manager-0.0.9.dev5.dist-info/WHEEL +5 -0
- rainbow_rb_flow_manager-0.0.9.dev5.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import contextlib
|
|
3
|
+
import queue
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import (
|
|
7
|
+
Callable,
|
|
8
|
+
MutableMapping,
|
|
9
|
+
)
|
|
10
|
+
from multiprocessing import (
|
|
11
|
+
Queue,
|
|
12
|
+
)
|
|
13
|
+
from multiprocessing.synchronize import (
|
|
14
|
+
Event as EventType,
|
|
15
|
+
)
|
|
16
|
+
from typing import Any, Literal, cast
|
|
17
|
+
|
|
18
|
+
from rainbow.rb_sdk.amr import RBAmrSDK
|
|
19
|
+
from rainbow.rb_sdk.base import RBBaseSDK
|
|
20
|
+
from rainbow.rb_sdk.manipulate import RBManipulateSDK
|
|
21
|
+
from rainbow.rb_sdk.program_sdk.program import RBProgramSDK
|
|
22
|
+
from rainbow.rb_utils.parser import t_to_dict
|
|
23
|
+
|
|
24
|
+
from .exception import BreakFolder, StopExecution, SubTaskHaltException
|
|
25
|
+
from .schema import ExecutorState, RB_Flow_Manager_ProgramState
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExecutionContext:
|
|
29
|
+
"""스크립트 실행 컨텍스트"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
process_id: str,
|
|
34
|
+
state_dict: dict[str, Any],
|
|
35
|
+
result_queue: Queue,
|
|
36
|
+
pause_event: EventType,
|
|
37
|
+
resume_event: EventType,
|
|
38
|
+
stop_event: EventType,
|
|
39
|
+
*,
|
|
40
|
+
min_step_interval: float | None = None,
|
|
41
|
+
step_mode: bool = False,
|
|
42
|
+
shared_state_dicts: dict[str, MutableMapping[str, Any]] | None = None,
|
|
43
|
+
):
|
|
44
|
+
self.process_id = process_id
|
|
45
|
+
self.state_dict: ExecutorState = cast(ExecutorState, state_dict)
|
|
46
|
+
self.parent_process_id = state_dict.get("parent_process_id")
|
|
47
|
+
self.pause_event = pause_event
|
|
48
|
+
self.resume_event = resume_event
|
|
49
|
+
self.stop_event = stop_event
|
|
50
|
+
self.result_queue = result_queue
|
|
51
|
+
self.variables: dict[str, dict[str, Any]] = {
|
|
52
|
+
"local": {},
|
|
53
|
+
"global": {},
|
|
54
|
+
}
|
|
55
|
+
self._dirty_local_variables: set[str] = set()
|
|
56
|
+
self._dirty_global_variables: set[str] = set()
|
|
57
|
+
self.sdk_functions: dict[str, Callable] = {}
|
|
58
|
+
self._sdk_roots: dict[str, Any] | None = None
|
|
59
|
+
self._arg_scope: list[dict[str, Any]] = []
|
|
60
|
+
self._generation = state_dict.get("generation")
|
|
61
|
+
self.min_step_interval = min_step_interval
|
|
62
|
+
self._folder_depth = 0
|
|
63
|
+
self.step_mode = step_mode
|
|
64
|
+
self.step_num = 1
|
|
65
|
+
self.current_depth = 0
|
|
66
|
+
self._step_ticket = 1 if not step_mode else 0
|
|
67
|
+
self.is_ui_execution = state_dict.get("is_ui_execution", False)
|
|
68
|
+
self.data: dict[str, Any] = {} # 사용자 정의 데이터 저장소
|
|
69
|
+
self.shared_state_dicts = shared_state_dicts
|
|
70
|
+
|
|
71
|
+
# parent_process_id가 있으면 부모 변수 저장소를 owner로 사용
|
|
72
|
+
self.sync_state_variables()
|
|
73
|
+
# 서브프로세스에서 첫 스텝 호출 시점의 lazy init 레이스/지연을 줄이기 위해
|
|
74
|
+
# ExecutionContext 생성 시 SDK 루트를 선초기화한다.
|
|
75
|
+
self._ensure_sdk_roots()
|
|
76
|
+
|
|
77
|
+
def _resolve_variable_owner_state(self) -> ExecutorState:
|
|
78
|
+
if self.parent_process_id is not None and self.shared_state_dicts is not None:
|
|
79
|
+
parent_state = self.shared_state_dicts.get(self.parent_process_id)
|
|
80
|
+
if parent_state is not None:
|
|
81
|
+
return cast(ExecutorState, parent_state)
|
|
82
|
+
return self.state_dict
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _normalize_state_variables(raw: Any) -> dict[str, dict[str, Any]]:
|
|
86
|
+
if not isinstance(raw, dict):
|
|
87
|
+
return {}
|
|
88
|
+
normalized: dict[str, dict[str, Any]] = {}
|
|
89
|
+
for key, payload in raw.items():
|
|
90
|
+
if not isinstance(payload, dict) or "value" not in payload:
|
|
91
|
+
continue
|
|
92
|
+
var_type = "global" if payload.get("type") == "global" else "local"
|
|
93
|
+
normalized[key] = {"value": payload.get("value"), "type": var_type}
|
|
94
|
+
return normalized
|
|
95
|
+
|
|
96
|
+
def update_vars_to_state_dict(self):
|
|
97
|
+
owner_state = self._resolve_variable_owner_state()
|
|
98
|
+
current = dict(owner_state.get("variables", {}))
|
|
99
|
+
|
|
100
|
+
for k in self._dirty_global_variables:
|
|
101
|
+
v = self.variables.get("global", {}).get(k)
|
|
102
|
+
current[k] = {"value": v, "type": "global"}
|
|
103
|
+
|
|
104
|
+
for k in self._dirty_local_variables:
|
|
105
|
+
v = self.variables.get("local", {}).get(k)
|
|
106
|
+
current[k] = {"value": v, "type": "local"}
|
|
107
|
+
|
|
108
|
+
owner_state["variables"] = current
|
|
109
|
+
# 로컬 state에도 미러링해 UI/호환성 유지
|
|
110
|
+
self.state_dict["variables"] = current
|
|
111
|
+
self._dirty_global_variables.clear()
|
|
112
|
+
self._dirty_local_variables.clear()
|
|
113
|
+
|
|
114
|
+
def sync_state_variables(self):
|
|
115
|
+
"""owner(state 혹은 parent state)의 variables를 런타임 캐시에 반영"""
|
|
116
|
+
owner_state = self._resolve_variable_owner_state()
|
|
117
|
+
normalized = self._normalize_state_variables(owner_state.get("variables", {}))
|
|
118
|
+
|
|
119
|
+
self.variables["local"].clear()
|
|
120
|
+
self.variables["global"].clear()
|
|
121
|
+
|
|
122
|
+
for key, payload in normalized.items():
|
|
123
|
+
if payload["type"] == "global":
|
|
124
|
+
self.variables["global"][key] = payload["value"]
|
|
125
|
+
else:
|
|
126
|
+
self.variables["local"][key] = payload["value"]
|
|
127
|
+
|
|
128
|
+
self.state_dict["variables"] = dict(owner_state.get("variables", {}))
|
|
129
|
+
|
|
130
|
+
def update_local_variables(self, variables: dict[str, Any]):
|
|
131
|
+
normalized = t_to_dict(variables)
|
|
132
|
+
self.variables["local"].update(normalized)
|
|
133
|
+
self._dirty_local_variables.update(normalized.keys())
|
|
134
|
+
self.update_vars_to_state_dict()
|
|
135
|
+
|
|
136
|
+
def update_global_variables(self, variables: dict[str, Any]):
|
|
137
|
+
self.variables["global"].update(variables)
|
|
138
|
+
self._dirty_global_variables.update(variables.keys())
|
|
139
|
+
self.update_vars_to_state_dict()
|
|
140
|
+
|
|
141
|
+
def mark_dirty_variables(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
local: set[str] | None = None,
|
|
145
|
+
global_: set[str] | None = None,
|
|
146
|
+
):
|
|
147
|
+
self._dirty_local_variables.update(local or set())
|
|
148
|
+
self._dirty_global_variables.update(global_ or set())
|
|
149
|
+
|
|
150
|
+
def get_global_variable(self, var_name: str) -> Any:
|
|
151
|
+
robot_model = self.state_dict.get("robot_model", None)
|
|
152
|
+
category = self.state_dict.get("category", None)
|
|
153
|
+
|
|
154
|
+
if category is None or robot_model is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
if category == "manipulate":
|
|
159
|
+
fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variable")
|
|
160
|
+
|
|
161
|
+
res = fn(robot_model, var_name) if fn is not None else None
|
|
162
|
+
|
|
163
|
+
if res is None:
|
|
164
|
+
raise RuntimeError(f"Failed to get global variable: {var_name}")
|
|
165
|
+
|
|
166
|
+
self.update_global_variables({var_name: res})
|
|
167
|
+
|
|
168
|
+
return res
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise e
|
|
171
|
+
|
|
172
|
+
def _ensure_sdk_roots(self):
|
|
173
|
+
if self._sdk_roots is not None:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
category = self.state_dict.get("category", None)
|
|
178
|
+
|
|
179
|
+
rb_base_sdk = RBBaseSDK()
|
|
180
|
+
rb_manipulate_sdk: RBManipulateSDK | None = None
|
|
181
|
+
rb_amr_sdk: RBAmrSDK | None = None
|
|
182
|
+
rb_program_sdk = RBProgramSDK()
|
|
183
|
+
|
|
184
|
+
if category == "manipulate":
|
|
185
|
+
rb_manipulate_sdk = RBManipulateSDK()
|
|
186
|
+
elif category == "amr":
|
|
187
|
+
rb_amr_sdk = RBAmrSDK()
|
|
188
|
+
|
|
189
|
+
self._sdk_roots = {
|
|
190
|
+
"rb_base_sdk": rb_base_sdk,
|
|
191
|
+
"rb_program_sdk": rb_program_sdk,
|
|
192
|
+
"rb_manipulate_sdk": rb_manipulate_sdk,
|
|
193
|
+
"rb_amr_sdk": rb_amr_sdk,
|
|
194
|
+
}
|
|
195
|
+
except Exception as e:
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
f"SDK initialization failed in process {self.process_id}: {e}"
|
|
198
|
+
) from e
|
|
199
|
+
|
|
200
|
+
def get_sdk_function(self, func_name: str) -> Callable | None:
|
|
201
|
+
if not func_name:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
cached = self.sdk_functions.get(func_name)
|
|
205
|
+
if cached is not None:
|
|
206
|
+
return cached
|
|
207
|
+
|
|
208
|
+
self._ensure_sdk_roots()
|
|
209
|
+
if self._sdk_roots is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
parts = func_name.split(".")
|
|
213
|
+
if len(parts) < 2:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
obj = self._sdk_roots.get(parts[0])
|
|
217
|
+
if obj is None:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
for part in parts[1:]:
|
|
221
|
+
if part.startswith("_") or not hasattr(obj, part):
|
|
222
|
+
return None
|
|
223
|
+
obj = getattr(obj, part)
|
|
224
|
+
|
|
225
|
+
if not callable(obj):
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
self.sdk_functions[func_name] = obj
|
|
229
|
+
return obj
|
|
230
|
+
|
|
231
|
+
def step_barrier(self):
|
|
232
|
+
"""step_mode면, 이 스텝 수행 후 일시정지하고
|
|
233
|
+
resume 신호 올 때까지 대기"""
|
|
234
|
+
if not self.step_mode or self.step_num < 2:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if self.stop_event.is_set():
|
|
238
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
|
|
239
|
+
raise StopExecution("Execution stopped by user")
|
|
240
|
+
|
|
241
|
+
# pause_event가 이미 set된 상태로 barrier에 들어와도
|
|
242
|
+
# executor/UI 상태가 RUNNING으로 남지 않도록 WAITING 이벤트를 보낸다.
|
|
243
|
+
if not self.pause_event.is_set():
|
|
244
|
+
self.pause_event.set()
|
|
245
|
+
self.emit_pause(self.state_dict["current_step_id"], is_wait=True)
|
|
246
|
+
self._wait_for_resume()
|
|
247
|
+
|
|
248
|
+
def push_args(self, mapping: dict[str, Any] | None):
|
|
249
|
+
self._arg_scope.append(mapping or {})
|
|
250
|
+
|
|
251
|
+
def pop_args(self):
|
|
252
|
+
if self._arg_scope:
|
|
253
|
+
self._arg_scope.pop()
|
|
254
|
+
|
|
255
|
+
def lookup(self, key: str) -> Any:
|
|
256
|
+
idx = len(self._arg_scope) - 2
|
|
257
|
+
|
|
258
|
+
while idx >= 0:
|
|
259
|
+
scope = self._arg_scope[idx]
|
|
260
|
+
if key in scope:
|
|
261
|
+
return scope[key]
|
|
262
|
+
idx -= 1
|
|
263
|
+
raise KeyError(f"parent pointer not found: {key}")
|
|
264
|
+
|
|
265
|
+
def pause(self, is_wait: bool = False):
|
|
266
|
+
"""현재 스크립트를 일시정지"""
|
|
267
|
+
if not self.pause_event.is_set():
|
|
268
|
+
self.pause_event.set()
|
|
269
|
+
self.emit_pause(self.state_dict["current_step_id"], is_wait)
|
|
270
|
+
|
|
271
|
+
self._wait_for_resume()
|
|
272
|
+
|
|
273
|
+
def resume(self):
|
|
274
|
+
"""현재 스크립트를 재개"""
|
|
275
|
+
if self.resume_event.is_set():
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self.resume_event.set()
|
|
279
|
+
self.pause_event.clear()
|
|
280
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
|
|
281
|
+
|
|
282
|
+
def stop(self):
|
|
283
|
+
"""현재 스크립트를 중지"""
|
|
284
|
+
if not self.stop_event.is_set():
|
|
285
|
+
self.stop_event.set()
|
|
286
|
+
self.emit_stop(self.state_dict["current_step_id"])
|
|
287
|
+
|
|
288
|
+
self.pause_event.clear()
|
|
289
|
+
|
|
290
|
+
self._wait_for_resume()
|
|
291
|
+
|
|
292
|
+
def _wait_for_resume(self):
|
|
293
|
+
if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
|
|
294
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
|
|
295
|
+
raise StopExecution("Execution stopped by user")
|
|
296
|
+
|
|
297
|
+
if self.state_dict.get("ignore_pause", False):
|
|
298
|
+
self.pause_event.clear()
|
|
299
|
+
self.resume_event.clear()
|
|
300
|
+
if self.state_dict.get("state") == RB_Flow_Manager_ProgramState.PAUSED:
|
|
301
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
if self.pause_event.is_set():
|
|
305
|
+
# resume이 이미 와 있으면 바로 통과 (레이스 방지)
|
|
306
|
+
if self.resume_event.is_set():
|
|
307
|
+
self.resume_event.clear()
|
|
308
|
+
self.pause_event.clear()
|
|
309
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.PAUSED
|
|
313
|
+
|
|
314
|
+
while True:
|
|
315
|
+
if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
|
|
316
|
+
self.pause_event.clear()
|
|
317
|
+
self.resume_event.clear()
|
|
318
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
|
|
319
|
+
raise StopExecution("Execution stopped by user")
|
|
320
|
+
|
|
321
|
+
if self.state_dict.get("ignore_pause", False):
|
|
322
|
+
self.pause_event.clear()
|
|
323
|
+
self.resume_event.clear()
|
|
324
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
if self.resume_event.wait(timeout=0.01):
|
|
328
|
+
self.resume_event.clear()
|
|
329
|
+
self.pause_event.clear()
|
|
330
|
+
if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
|
|
331
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
|
|
332
|
+
raise StopExecution("Execution stopped by user")
|
|
333
|
+
self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
def check_stop(self):
|
|
337
|
+
"""stop/pause/resume 명령 처리"""
|
|
338
|
+
self._wait_for_resume() # 즉시 멈춤/중지 반영
|
|
339
|
+
|
|
340
|
+
def emit_next(self, step_id: str):
|
|
341
|
+
"""next 이벤트 발생"""
|
|
342
|
+
try:
|
|
343
|
+
self.result_queue.put_nowait(
|
|
344
|
+
{
|
|
345
|
+
"type": "next",
|
|
346
|
+
"process_id": self.process_id,
|
|
347
|
+
"step_id": step_id,
|
|
348
|
+
"ts": time.time(),
|
|
349
|
+
"generation": self._generation,
|
|
350
|
+
"error": None,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
except queue.Full:
|
|
354
|
+
# 고빈도 next 이벤트는 큐 포화 시 드롭해 stop 반응성을 보장한다.
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
def emit_pause(self, step_id: str, is_wait: bool = False):
|
|
358
|
+
"""pause 이벤트 발생"""
|
|
359
|
+
self.result_queue.put(
|
|
360
|
+
{
|
|
361
|
+
"type": "pause",
|
|
362
|
+
"process_id": self.process_id,
|
|
363
|
+
"step_id": step_id,
|
|
364
|
+
"is_wait": is_wait,
|
|
365
|
+
"parent_process_id": self.parent_process_id,
|
|
366
|
+
"ts": time.time(),
|
|
367
|
+
"generation": self._generation,
|
|
368
|
+
"error": None,
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def emit_wait(self, step_id: str):
|
|
373
|
+
self.result_queue.put(
|
|
374
|
+
{
|
|
375
|
+
"type": "wait",
|
|
376
|
+
"process_id": self.process_id,
|
|
377
|
+
"step_id": step_id,
|
|
378
|
+
"parent_process_id": self.parent_process_id,
|
|
379
|
+
"ts": time.time(),
|
|
380
|
+
"generation": self._generation,
|
|
381
|
+
"error": None,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def emit_resume(self, step_id: str):
|
|
386
|
+
"""resume 이벤트 발생"""
|
|
387
|
+
self.result_queue.put(
|
|
388
|
+
{
|
|
389
|
+
"type": "resume",
|
|
390
|
+
"process_id": self.process_id,
|
|
391
|
+
"step_id": step_id,
|
|
392
|
+
"parent_process_id": self.parent_process_id,
|
|
393
|
+
"ts": time.time(),
|
|
394
|
+
"generation": self._generation,
|
|
395
|
+
"error": None,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def emit_stop(self, step_id: str):
|
|
400
|
+
"""stop 이벤트 발생"""
|
|
401
|
+
self.result_queue.put(
|
|
402
|
+
{
|
|
403
|
+
"type": "stop",
|
|
404
|
+
"process_id": self.process_id,
|
|
405
|
+
"step_id": step_id,
|
|
406
|
+
"parent_process_id": self.parent_process_id,
|
|
407
|
+
"ts": time.time(),
|
|
408
|
+
"generation": self._generation,
|
|
409
|
+
"error": None,
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def emit_error(self, step_id: str, error: Exception):
|
|
414
|
+
"""error 이벤트 발생"""
|
|
415
|
+
|
|
416
|
+
self.result_queue.put(
|
|
417
|
+
{
|
|
418
|
+
"type": "error",
|
|
419
|
+
"process_id": self.process_id,
|
|
420
|
+
"step_id": step_id,
|
|
421
|
+
"ts": time.time(),
|
|
422
|
+
"generation": self._generation,
|
|
423
|
+
"error": str(error),
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def emit_subtask_sync_register(
|
|
428
|
+
self, sub_task_tree, post_tree=None, subtask_type: Literal["INSERT", "CHANGE"] = "INSERT"
|
|
429
|
+
):
|
|
430
|
+
"""서브태스크 sync flags 등록 요청 이벤트"""
|
|
431
|
+
self.result_queue.put(
|
|
432
|
+
{
|
|
433
|
+
"type": "subtask_sync_register",
|
|
434
|
+
"process_id": self.process_id,
|
|
435
|
+
"subtask_type": subtask_type,
|
|
436
|
+
"sub_task_tree": sub_task_tree.to_dict() if sub_task_tree else None,
|
|
437
|
+
"post_tree": post_tree.to_dict() if post_tree else None,
|
|
438
|
+
"ts": time.time(),
|
|
439
|
+
"generation": self._generation,
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def emit_subtask_sync_unregister(
|
|
444
|
+
self, sub_task_tree, post_tree=None, subtask_type: Literal["INSERT", "CHANGE"] = "INSERT"
|
|
445
|
+
):
|
|
446
|
+
"""서브태스크 sync flags 해제 요청 이벤트"""
|
|
447
|
+
self.result_queue.put(
|
|
448
|
+
{
|
|
449
|
+
"type": "subtask_sync_unregister",
|
|
450
|
+
"process_id": self.process_id,
|
|
451
|
+
"subtask_type": subtask_type,
|
|
452
|
+
"sub_task_tree": sub_task_tree.to_dict() if sub_task_tree else None,
|
|
453
|
+
"post_tree": post_tree.to_dict() if post_tree else None,
|
|
454
|
+
"ts": time.time(),
|
|
455
|
+
"generation": self._generation,
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def emit_main_tree_sync_unregister(self):
|
|
460
|
+
"""메인 트리 sync flags 해제 요청 (CHANGE 타입 전환 시)"""
|
|
461
|
+
self.result_queue.put(
|
|
462
|
+
{
|
|
463
|
+
"type": "main_tree_sync_unregister",
|
|
464
|
+
"process_id": self.process_id,
|
|
465
|
+
"ts": time.time(),
|
|
466
|
+
"generation": self._generation,
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
def emit_sub_task_start(self, task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]):
|
|
471
|
+
"""sub_task_start 이벤트 발생"""
|
|
472
|
+
self.result_queue.put(
|
|
473
|
+
{
|
|
474
|
+
"type": "sub_task_start",
|
|
475
|
+
"process_id": self.process_id,
|
|
476
|
+
"sub_task_id": task_id,
|
|
477
|
+
"sub_task_type": sub_task_type,
|
|
478
|
+
"ts": time.time(),
|
|
479
|
+
"generation": self._generation,
|
|
480
|
+
"error": None,
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def emit_sub_task_done(self, task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]):
|
|
485
|
+
"""sub_task_done 이벤트 발생"""
|
|
486
|
+
self.result_queue.put(
|
|
487
|
+
{
|
|
488
|
+
"type": "sub_task_done",
|
|
489
|
+
"process_id": self.process_id,
|
|
490
|
+
"sub_task_id": task_id,
|
|
491
|
+
"sub_task_type": sub_task_type,
|
|
492
|
+
"ts": time.time(),
|
|
493
|
+
"generation": self._generation,
|
|
494
|
+
"error": None,
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def emit_event_sub_task_start(
|
|
499
|
+
self,
|
|
500
|
+
*,
|
|
501
|
+
event_task_id: str,
|
|
502
|
+
event_tree,
|
|
503
|
+
step_id: str | None = None,
|
|
504
|
+
run_mode: Literal["SYNC", "ASYNC"] = "ASYNC",
|
|
505
|
+
call_seq: int | None = None,
|
|
506
|
+
):
|
|
507
|
+
"""EventSubTask 실행 요청 이벤트"""
|
|
508
|
+
self.result_queue.put(
|
|
509
|
+
{
|
|
510
|
+
"type": "event_sub_task_start",
|
|
511
|
+
"process_id": self.process_id,
|
|
512
|
+
"event_task_id": event_task_id,
|
|
513
|
+
"event_tree": event_tree.to_dict() if event_tree is not None else None,
|
|
514
|
+
"step_id": step_id,
|
|
515
|
+
"run_mode": run_mode,
|
|
516
|
+
"call_seq": call_seq,
|
|
517
|
+
"ts": time.time(),
|
|
518
|
+
"generation": self._generation,
|
|
519
|
+
"error": None,
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def emit_post_start(self):
|
|
524
|
+
"""post_start 이벤트 발생"""
|
|
525
|
+
self.result_queue.put(
|
|
526
|
+
{
|
|
527
|
+
"type": "post_start",
|
|
528
|
+
"process_id": self.process_id,
|
|
529
|
+
"ts": time.time(),
|
|
530
|
+
"generation": self._generation,
|
|
531
|
+
"error": None,
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def emit_done(self, step_id: str):
|
|
536
|
+
"""done 이벤트 발생"""
|
|
537
|
+
try:
|
|
538
|
+
self.result_queue.put_nowait(
|
|
539
|
+
{
|
|
540
|
+
"type": "done",
|
|
541
|
+
"process_id": self.process_id,
|
|
542
|
+
"step_id": step_id,
|
|
543
|
+
"ts": time.time(),
|
|
544
|
+
"generation": self._generation,
|
|
545
|
+
"error": None,
|
|
546
|
+
}
|
|
547
|
+
)
|
|
548
|
+
except queue.Full:
|
|
549
|
+
# 고빈도 done 이벤트는 큐 포화 시 드롭해 stop 반응성을 보장한다.
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
def pause_all(self):
|
|
553
|
+
self.result_queue.put(
|
|
554
|
+
{
|
|
555
|
+
"type": "control",
|
|
556
|
+
"action": "pause_all",
|
|
557
|
+
"process_id": self.process_id,
|
|
558
|
+
"ts": time.time(),
|
|
559
|
+
"generation": self._generation,
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
def resume_all(self):
|
|
564
|
+
self.result_queue.put(
|
|
565
|
+
{
|
|
566
|
+
"type": "control",
|
|
567
|
+
"action": "resume_all",
|
|
568
|
+
"process_id": self.process_id,
|
|
569
|
+
"ts": time.time(),
|
|
570
|
+
"generation": self._generation,
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def enter_folder(self):
|
|
575
|
+
"""Folder 진입"""
|
|
576
|
+
self._folder_depth += 1
|
|
577
|
+
|
|
578
|
+
def leave_folder(self):
|
|
579
|
+
"""Folder 탈출"""
|
|
580
|
+
self._folder_depth -= 1
|
|
581
|
+
|
|
582
|
+
def break_folder(self):
|
|
583
|
+
"""Folder 실행 중단"""
|
|
584
|
+
if self._folder_depth > 0:
|
|
585
|
+
raise BreakFolder()
|
|
586
|
+
|
|
587
|
+
def halt_sub_task(self):
|
|
588
|
+
"""서브 태스크 실행 중단"""
|
|
589
|
+
raise SubTaskHaltException()
|
|
590
|
+
|
|
591
|
+
def _safe_close_sdk(self):
|
|
592
|
+
"""SDK 안전 종료"""
|
|
593
|
+
with contextlib.suppress(Exception):
|
|
594
|
+
if self._sdk_roots is not None:
|
|
595
|
+
for sdk in self._sdk_roots.values():
|
|
596
|
+
close_fn = getattr(sdk, "close", None)
|
|
597
|
+
if callable(close_fn):
|
|
598
|
+
close_fn()
|
|
599
|
+
|
|
600
|
+
def close(self):
|
|
601
|
+
"""안전하게 close"""
|
|
602
|
+
self._safe_close_sdk()
|
|
603
|
+
|
|
604
|
+
def __del__(self):
|
|
605
|
+
"""소멸자"""
|
|
606
|
+
if getattr(sys, "is_finalizing", lambda: False)():
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
with contextlib.suppress(builtins.BaseException):
|
|
610
|
+
self.close()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from collections.abc import (
|
|
6
|
+
MutableMapping,
|
|
7
|
+
)
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseController(ABC):
|
|
12
|
+
"""모든 컨트롤러가 따라야 하는 공통 인터페이스"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def on_init(self, state_dicts: dict[str, MutableMapping[str, Any]]) -> None:
|
|
16
|
+
"""컨트롤러 초기화"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def on_start(self, task_id: str) -> None:
|
|
20
|
+
"""스크립트/플로우 시작 시점에 한 번 호출"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def on_stop(self, task_id: str, step_id: str) -> None:
|
|
24
|
+
"""각 Step이 Stop 될때 동작을 수행"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def on_pause(self, task_id: str, step_id: str) -> None:
|
|
28
|
+
"""각 Step이 Pause 될때 실제 동작을 수행"""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def on_wait(self, task_id: str, step_id: str) -> None:
|
|
32
|
+
"""각 Step이 Wait 될때 실제 동작을 수행"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def on_resume(self, task_id: str, step_id: str) -> None:
|
|
36
|
+
"""각 Step이 Resume 될때 실제 동작을 수행"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def on_next(self, task_id: str, step_id: str) -> None:
|
|
40
|
+
"""각 Step이 다음 Step으로 이동 할때 동작을 수행"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def on_sub_task_start(self, task_id: str, sub_task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]) -> None:
|
|
44
|
+
"""각 Step이 서브 태스크 시작 시점에 동작을 수행"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def on_sub_task_done(self, task_id: str, sub_task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]) -> None:
|
|
48
|
+
"""각 Step이 서브 태스크 완료 시점에 동작을 수행"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def on_done(self, task_id: str, step_id: str) -> None:
|
|
52
|
+
"""각 Step이 완료 되었을때 동작을 수행"""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def on_error(self, task_id: str, step_id: str, error: Exception) -> None:
|
|
56
|
+
"""각 Step에서 Error 발생 시 동작을 수행"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def on_post_start(self, task_id: str) -> None:
|
|
60
|
+
"""스크립트/플로우 Post Tree의 시작 시점에 동작을 수행"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def on_complete(self, task_id: str) -> None:
|
|
64
|
+
"""스크립트/플로우 완료 시점에 동작을 수행"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def on_close(self) -> None:
|
|
68
|
+
"""컨트롤러 종료"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def on_all_complete(self) -> None:
|
|
72
|
+
"""모든 스크립트/플로우 완료 시점에 동작을 수행"""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def on_all_stop(self) -> None:
|
|
76
|
+
"""모든 스크립트/플로우 Stop 시점에 동작을 수행"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def on_all_pause(self) -> None:
|
|
80
|
+
"""모든 스크립트/플로우 Pause 시점에 동작을 수행"""
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def on_all_wait(self) -> None:
|
|
84
|
+
"""모든 스크립트/플로우 Wait 시점에 동작을 수행"""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def on_all_resume(self) -> None:
|
|
88
|
+
"""모든 스크립트/플로우 Resume 시점에 동작을 수행"""
|