proc-executor 0.1.2__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,1031 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proc-executor
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Reusable multiprocessing execution support for controller classes
|
|
5
|
+
Author-email: Kengo NAKADA <kengo.nakada@email.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: xlogger-py
|
|
9
|
+
Requires-Dist: dataclasses
|
|
10
|
+
Requires-Dist: coloredlogs
|
|
11
|
+
Requires-Dist: colorlog
|
|
12
|
+
Requires-Dist: pytest
|
|
13
|
+
Requires-Dist: pytest-asyncio
|
|
14
|
+
Requires-Dist: pyyaml
|
|
15
|
+
Requires-Dist: toml
|
|
16
|
+
Requires-Dist: nest_asyncio
|
|
17
|
+
Requires-Dist: sphinx
|
|
18
|
+
Requires-Dist: myst-parser
|
|
19
|
+
Requires-Dist: sphinx-autobuild
|
|
20
|
+
Requires-Dist: sphinx-rtd-theme
|
|
21
|
+
Requires-Dist: sphinxcontrib-napoleon
|
|
22
|
+
Requires-Dist: linkify-it-py
|
|
23
|
+
|
|
24
|
+
# process-executor
|
|
25
|
+
|
|
26
|
+
`process-executor` は、
|
|
27
|
+
通常の Python クラスを multiprocessing
|
|
28
|
+
の worker process 上で実行するための汎用実行基盤である。
|
|
29
|
+
特に、すでに単体で完成している装置制御用 Ctrl クラスを、
|
|
30
|
+
multiprocessing のためだけに書き直さず利用することを主目的としている。
|
|
31
|
+
|
|
32
|
+
設計・方針自体も含めて検討中コードであるが、運用しながら詰めていくことになる。
|
|
33
|
+
|
|
34
|
+
本 README は導入手順だけではなく、
|
|
35
|
+
現在の設計理由、
|
|
36
|
+
責務分離、
|
|
37
|
+
内部構造、
|
|
38
|
+
public API、
|
|
39
|
+
制約、および実際の example をまとめた仕様書兼チュートリアルである。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### インストール
|
|
46
|
+
|
|
47
|
+
repository root で実行する。
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 最小構成
|
|
54
|
+
|
|
55
|
+
既存クラスを `worker_class` に渡す。
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from process_executor import ProcessExecutor
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Calculator:
|
|
62
|
+
def add(self, a: int, b: int) -> int:
|
|
63
|
+
return a + b
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
with ProcessExecutor(
|
|
68
|
+
worker_class=Calculator,
|
|
69
|
+
worker_count=2,
|
|
70
|
+
start_method="spawn",
|
|
71
|
+
) as executor:
|
|
72
|
+
job_id = executor.submit("add", 10, 20)
|
|
73
|
+
result = executor.wait(job_id, timeout_sec=5.0)
|
|
74
|
+
print(result)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`Calculator` 自体には multiprocessing 用の実装を追加しない。各 worker process 内で `Calculator()` が生成され、`submit("add", 10, 20)` によって、その worker-local instance の `add(10, 20)` が実行される。
|
|
82
|
+
|
|
83
|
+
### 非同期に job を扱う
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
job_id = executor.submit("add", 10, 20)
|
|
87
|
+
|
|
88
|
+
print(executor.state(job_id))
|
|
89
|
+
print(executor.job(job_id))
|
|
90
|
+
print(executor.workers())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`result()` は待機しない。完了待機が必要な場合は `wait()` を使用する。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## README の読み方
|
|
98
|
+
|
|
99
|
+
この README は次の順序で読むことを想定している。
|
|
100
|
+
|
|
101
|
+
1. **Quick Start** — 最小構成で実行する。
|
|
102
|
+
2. **なぜこのプログラムを作ったのか** — 導入目的を理解する。
|
|
103
|
+
3. **設計の中心** — 既存 Ctrl と multiprocessing を分離する理由を理解する。
|
|
104
|
+
4. **worker / job / SharedContext** — 内部実行モデルを理解する。
|
|
105
|
+
5. **example** — generic class、既存制御 class、PM2CD での具体的利用方法を見る。
|
|
106
|
+
6. **追加設計が必要なケース / 責務境界** — ProcessExecutor が意図的に扱わない範囲を確認する。
|
|
107
|
+
|
|
108
|
+
GitHub Pages では、この手動ドキュメントに加えて `src/process_executor` を Sphinx で機械的に解析した API reference を生成する。
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 1. なぜこのプログラムを作ったのか
|
|
113
|
+
|
|
114
|
+
装置制御コードは、まず通常の同期クラスとして実装するのが自然である。
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
class MotorCtrl:
|
|
118
|
+
def move(
|
|
119
|
+
self,
|
|
120
|
+
motor: int,
|
|
121
|
+
pulse: int,
|
|
122
|
+
) -> bool:
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def get_position(
|
|
126
|
+
self,
|
|
127
|
+
motor: int,
|
|
128
|
+
) -> int:
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
このクラスの責務は、装置 protocol、command、状態取得、移動、停止などである。
|
|
133
|
+
|
|
134
|
+
一方、制御システムを大きくすると、装置 protocol とは別に次の要求が発生する。
|
|
135
|
+
|
|
136
|
+
- 長時間処理を呼び出し元から切り離したい。
|
|
137
|
+
- 処理を job として投入し、job ID を受け取りたい。
|
|
138
|
+
- job の `queued`、`running`、`succeeded`、`failed` を管理したい。
|
|
139
|
+
- 複数 worker process を事前起動し、再利用したい。
|
|
140
|
+
- worker の状態を親 process から確認したい。
|
|
141
|
+
- worker 内で発生した例外を job の失敗として保持したい。
|
|
142
|
+
- FastAPI 等の上位 server から、job 投入と結果取得を分離したい。
|
|
143
|
+
- 装置ごとに `Process`、`Queue`、`Manager`、job 管理を再実装したくない。
|
|
144
|
+
|
|
145
|
+
これらは「装置をどう動かすか」という Ctrl 本来の責務ではない。
|
|
146
|
+
|
|
147
|
+
各 Ctrl に multiprocessing の実装を直接書き込むと、装置固有コードと process 管理コードが密結合する。また、装置が増えるたびに worker loop、job queue、状態管理をコピーすることになる。
|
|
148
|
+
|
|
149
|
+
`process-executor` は、この multiprocessing 側の共通部分を独立させるために作ったものである。
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 2. 設計の中心
|
|
154
|
+
|
|
155
|
+
現在の実装の中心的な方針は次である。
|
|
156
|
+
|
|
157
|
+
> Ctrl を multiprocessing 用に作り直すのではなく、既存 Ctrl を worker process 内で生成し、その public method を job として実行する。
|
|
158
|
+
|
|
159
|
+
概念的な構造は次である。
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
Application / Server
|
|
163
|
+
|
|
|
164
|
+
v
|
|
165
|
+
Multi Ctrl / Facade
|
|
166
|
+
|
|
|
167
|
+
v
|
|
168
|
+
ProcessExecutor
|
|
169
|
+
|
|
|
170
|
+
+-- job queue
|
|
171
|
+
+-- job metadata
|
|
172
|
+
+-- worker metadata
|
|
173
|
+
+-- shared state
|
|
174
|
+
+-- shared lock
|
|
175
|
+
|
|
|
176
|
+
+-- Worker Process 1
|
|
177
|
+
| |
|
|
178
|
+
| +-- Ctrl instance
|
|
179
|
+
|
|
|
180
|
+
+-- Worker Process 2
|
|
181
|
+
|
|
|
182
|
+
+-- Ctrl instance
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`ProcessExecutor` は装置 protocol、TCP、serial、モーター、ステージ等を知らない。
|
|
186
|
+
|
|
187
|
+
worker process 内では概念的に次を実行するだけである。
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
handler = getattr(instance, handler_name)
|
|
191
|
+
result = handler(*args, **kwargs)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
したがって、`ProcessExecutor` は装置非依存である。
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 3. なぜ worker 専用 Ctrl を作らないのか
|
|
199
|
+
|
|
200
|
+
既存 Ctrl がすでに次のように存在するとする。
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
class ExistingCtrl:
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
host: str,
|
|
207
|
+
) -> None:
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
def move(
|
|
211
|
+
self,
|
|
212
|
+
position: int,
|
|
213
|
+
) -> bool:
|
|
214
|
+
...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
multiprocessing のためだけに、
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
ExistingCtrl
|
|
221
|
+
↓
|
|
222
|
+
ExistingWorkerCtrl
|
|
223
|
+
↓
|
|
224
|
+
ExistingWorkerFactory
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
という専用クラス群を作ると、既存 Ctrl とほぼ同じ constructor や delegation を繰り返すことになる。
|
|
228
|
+
|
|
229
|
+
現在の実装では、この重複を避けるため `worker_class` I/F を持つ。
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
executor = ProcessExecutor(
|
|
233
|
+
worker_class=ExistingCtrl,
|
|
234
|
+
worker_kwargs={
|
|
235
|
+
"host": "192.168.0.10",
|
|
236
|
+
},
|
|
237
|
+
worker_count=2,
|
|
238
|
+
start_method="spawn",
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
各 worker process 内で `ExistingCtrl(...)` が生成される。
|
|
243
|
+
|
|
244
|
+
既存 Ctrl は multiprocessing を意識する必要がない。
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 4. 継承ではなく composition / delegation を使う理由
|
|
249
|
+
|
|
250
|
+
装置側 Ctrl が `ProcessExecutor` を継承する設計にはしていない。
|
|
251
|
+
|
|
252
|
+
関係は次である。
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
Multi Ctrl
|
|
256
|
+
has-a
|
|
257
|
+
ProcessExecutor
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
すなわち composition / delegation である。
|
|
261
|
+
|
|
262
|
+
この設計により、次を維持できる。
|
|
263
|
+
|
|
264
|
+
- 装置 Ctrl の class hierarchy を multiprocessing に支配されない。
|
|
265
|
+
- 既存 Ctrl を変更せず利用できる。
|
|
266
|
+
- ProcessExecutor を装置非依存に保てる。
|
|
267
|
+
- multi 側 public API を装置ごとに自由に設計できる。
|
|
268
|
+
- ProcessExecutor を別の制御系にも再利用できる。
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 5. worker の考え方
|
|
273
|
+
|
|
274
|
+
worker は handler 専用 process ではない。
|
|
275
|
+
|
|
276
|
+
`move()` 用 process、`get_position()` 用 process、`query()` 用 processのような構成にはしない。
|
|
277
|
+
|
|
278
|
+
一つの worker は一つの worker-local instance を保持し、queue から受け取った job に応じて異なる method を実行する。
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
Worker 1
|
|
282
|
+
Ctrl instance
|
|
283
|
+
|- query()
|
|
284
|
+
|- get()
|
|
285
|
+
|- move()
|
|
286
|
+
`- ...
|
|
287
|
+
|
|
288
|
+
Worker 2
|
|
289
|
+
Ctrl instance
|
|
290
|
+
|- query()
|
|
291
|
+
|- get()
|
|
292
|
+
|- move()
|
|
293
|
+
`- ...
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
handler の種類と worker の寿命を分離する設計である。
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 6. worker instance の生成方法
|
|
301
|
+
|
|
302
|
+
worker-local instance には二つの生成方法がある。
|
|
303
|
+
|
|
304
|
+
### 6.1 `worker_class`
|
|
305
|
+
|
|
306
|
+
通常の既存クラスをそのまま worker にする場合に使用する。
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
executor = ProcessExecutor(
|
|
310
|
+
worker_class=MyCtrl,
|
|
311
|
+
worker_args=(arg1, arg2),
|
|
312
|
+
worker_kwargs={
|
|
313
|
+
"option": value,
|
|
314
|
+
},
|
|
315
|
+
worker_count=2,
|
|
316
|
+
start_method="spawn",
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
各 worker process 内で概念的に次が実行される。
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
instance = MyCtrl(
|
|
324
|
+
*worker_args,
|
|
325
|
+
**worker_kwargs,
|
|
326
|
+
)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
既存 Ctrl をそのまま worker-local instance として使用する場合はこちらが基本である。
|
|
330
|
+
|
|
331
|
+
### 6.2 `worker_factory`
|
|
332
|
+
|
|
333
|
+
worker の生成時に `SharedContext` が必要な場合に使用する。
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
def create_ctrl(
|
|
337
|
+
context: SharedContext,
|
|
338
|
+
) -> MyCtrl:
|
|
339
|
+
return MyCtrl(
|
|
340
|
+
shared_state=context.state,
|
|
341
|
+
lock=context.lock,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
executor = ProcessExecutor(
|
|
346
|
+
worker_factory=create_ctrl,
|
|
347
|
+
worker_count=2,
|
|
348
|
+
start_method="spawn",
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`worker_factory(context)` は、共有 state や process-safe lock を worker instance に渡したい場合の I/F である。
|
|
353
|
+
|
|
354
|
+
### 6.3 両者の関係
|
|
355
|
+
|
|
356
|
+
`worker_factory` と `worker_class` は同時に指定できない。
|
|
357
|
+
|
|
358
|
+
`worker_args` と `worker_kwargs` は `worker_class` 専用である。
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 7. job の実行モデル
|
|
363
|
+
|
|
364
|
+
job は `submit()` で投入する。
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
job_id = executor.submit(
|
|
368
|
+
"move",
|
|
369
|
+
motor,
|
|
370
|
+
pulse,
|
|
371
|
+
)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`submit()` は handler の完了を待たない。返すのは handler の戻り値ではなく job ID である。
|
|
375
|
+
|
|
376
|
+
状態遷移は次である。
|
|
377
|
+
|
|
378
|
+
```text
|
|
379
|
+
queued
|
|
380
|
+
|
|
|
381
|
+
v
|
|
382
|
+
running
|
|
383
|
+
|
|
|
384
|
+
+--> succeeded
|
|
385
|
+
|
|
|
386
|
+
`--> failed
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
job metadata には次を保持する。
|
|
390
|
+
|
|
391
|
+
- `job_id`
|
|
392
|
+
- `handler`
|
|
393
|
+
- `args`
|
|
394
|
+
- `kwargs`
|
|
395
|
+
- `state`
|
|
396
|
+
- `worker_id`
|
|
397
|
+
- `result`
|
|
398
|
+
- `error`
|
|
399
|
+
- `created_at`
|
|
400
|
+
- `started_at`
|
|
401
|
+
- `finished_at`
|
|
402
|
+
|
|
403
|
+
一つの job は `job(job_id)`、全 job は `jobs()` で取得する。
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 8. `submit()`、`result()`、`wait()` の役割
|
|
408
|
+
|
|
409
|
+
### `submit(handler, *args, **kwargs) -> str`
|
|
410
|
+
|
|
411
|
+
job を queue に投入し、job ID を返す。
|
|
412
|
+
|
|
413
|
+
### `result(job_id)`
|
|
414
|
+
|
|
415
|
+
完了済み job の結果を取得する。
|
|
416
|
+
|
|
417
|
+
この method は待機しない。まだ `succeeded` していない場合は `RuntimeError` になる。
|
|
418
|
+
|
|
419
|
+
### `wait(job_id, timeout_sec=None, interval_sec=0.01)`
|
|
420
|
+
|
|
421
|
+
job の完了を明示的に待つ。
|
|
422
|
+
|
|
423
|
+
`succeeded` なら handler の戻り値を返す。
|
|
424
|
+
|
|
425
|
+
`failed` なら `RuntimeError` を送出する。
|
|
426
|
+
|
|
427
|
+
`timeout_sec` を超えると `TimeoutError` を送出する。
|
|
428
|
+
|
|
429
|
+
この分離により、上位 Ctrl は用途に応じて非同期 API と同期 API のどちらも構成できる。
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 9. worker metadata
|
|
434
|
+
|
|
435
|
+
worker metadata は次を持つ。
|
|
436
|
+
|
|
437
|
+
- `worker_id`
|
|
438
|
+
- `pid`
|
|
439
|
+
- `state`
|
|
440
|
+
- `current_job_id`
|
|
441
|
+
- `current_handler`
|
|
442
|
+
|
|
443
|
+
worker state は次である。
|
|
444
|
+
|
|
445
|
+
- `idle`
|
|
446
|
+
- `running`
|
|
447
|
+
- `stopped`
|
|
448
|
+
- `dead`
|
|
449
|
+
|
|
450
|
+
一つの worker は `worker(worker_id)`、全 worker は `workers()` で取得する。
|
|
451
|
+
|
|
452
|
+
`submit()`、`worker()`、`workers()` の実行時には dead worker を確認し、設定した `worker_count` を下回っていれば補充する。
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 10. SharedContext
|
|
457
|
+
|
|
458
|
+
`SharedContext` は次の共有 object をまとめた container である。
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
context.jobs
|
|
462
|
+
context.workers
|
|
463
|
+
context.state
|
|
464
|
+
context.lock
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `jobs`
|
|
468
|
+
|
|
469
|
+
ProcessExecutor が管理する job metadata の共有 mapping である。
|
|
470
|
+
|
|
471
|
+
### `workers`
|
|
472
|
+
|
|
473
|
+
ProcessExecutor が管理する worker metadata の共有 mapping である。
|
|
474
|
+
|
|
475
|
+
### `state`
|
|
476
|
+
|
|
477
|
+
利用側が必要に応じて使用する共有 mapping である。
|
|
478
|
+
|
|
479
|
+
親 process 側からは `ProcessExecutor.shared_context` を通して同じ `SharedContext` を取得できる。
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
context = executor.shared_context
|
|
483
|
+
context.state["key"] = value
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### `lock`
|
|
487
|
+
|
|
488
|
+
`multiprocessing.Manager().RLock()` により生成する process-safe lock である。
|
|
489
|
+
|
|
490
|
+
この lock は `motion_lock` や `communication_lock` のような装置固有名を持たない。どの handler が lock を使うかは利用側 Ctrl の責務である。
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 11. 例外処理
|
|
495
|
+
|
|
496
|
+
worker-local instance の handler 実行中に `Exception` が発生すると job は `failed` になる。
|
|
497
|
+
|
|
498
|
+
error metadata には次を保存する。
|
|
499
|
+
|
|
500
|
+
- exception type
|
|
501
|
+
- message
|
|
502
|
+
- traceback
|
|
503
|
+
|
|
504
|
+
failed job に対して `result()` または `wait()` を呼ぶと `RuntimeError` を送出する。
|
|
505
|
+
|
|
506
|
+
現在の実装では、worker 側で発生した元 exception class を親 process 側で再構築して再送出する仕様にはしていない。
|
|
507
|
+
|
|
508
|
+
なお、この exception handling の対象は handler 実行部分である。`worker_factory(context)` または `worker_class(...)` による worker-local instance の生成時に例外が発生した場合、その例外を job failure として親 process に通知する機構は現在の実装にはない。instance 生成に失敗した worker process は終了する。
|
|
509
|
+
|
|
510
|
+
また、handler 実行中に process 自体が強制終了するなど、Python の通常の `Exception` handling を通らず worker が消滅した場合、その時点で `running` であった job を自動的に `failed` へ変更する機構も現在の実装にはない。そのため、このような job に対して timeout を指定せず `wait()` すると待機が継続する可能性がある。
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 12. worker lifecycle
|
|
515
|
+
|
|
516
|
+
constructor では指定数の worker process を起動する。
|
|
517
|
+
|
|
518
|
+
現在の実装は、constructor が各 worker の `idle` 登録完了まで待機する仕様ではない。worker process は起動後に worker-local instance を生成し、その後 `idle` として metadata を登録する。
|
|
519
|
+
|
|
520
|
+
job は worker の準備前でも queue に投入でき、worker が利用可能になった時点で処理される。
|
|
521
|
+
|
|
522
|
+
`close()` は生存 worker に stop sentinel を投入し、worker を `join()` した後、Queue と Manager を終了する。
|
|
523
|
+
|
|
524
|
+
現在の `join()` には timeout を指定していない。そのため、worker が戻らない handler を実行中である場合、`close()` も worker 終了まで待機し続ける可能性がある。
|
|
525
|
+
|
|
526
|
+
worker process は `daemon=True` で起動する。したがって、worker-local handler からさらに multiprocessing child process を生成する用途を前提にはしていない。
|
|
527
|
+
|
|
528
|
+
`close()` は複数回呼んでも二回目以降は何もしない。
|
|
529
|
+
|
|
530
|
+
context manager としても使用できる。
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
with ProcessExecutor(
|
|
534
|
+
worker_class=MyCtrl,
|
|
535
|
+
) as executor:
|
|
536
|
+
...
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
終了 API は `close()` であり、`shutdown()` ではない。
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## 13. logger
|
|
544
|
+
|
|
545
|
+
`ProcessExecutor` constructor は `logger` を受け取る。
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
executor = ProcessExecutor(
|
|
549
|
+
worker_class=MyCtrl,
|
|
550
|
+
logger=logger,
|
|
551
|
+
)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
現在の実装では logger を親 process 側に保持するのみであり、ProcessExecutor core から worker process へ自動注入しない。
|
|
555
|
+
|
|
556
|
+
複数 process から同じ rotating log file を直接扱う logging aggregation は ProcessExecutor core の責務には含めていない。
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## 14. directory 構成
|
|
561
|
+
|
|
562
|
+
source と example の主要構成は次である。`build/`、`*.egg-info`、`__pycache__/` は生成物なので省略する。
|
|
563
|
+
|
|
564
|
+
```text
|
|
565
|
+
process-executor/
|
|
566
|
+
|- pyproject.toml
|
|
567
|
+
|- README.md
|
|
568
|
+
|- CHANGELOG.md
|
|
569
|
+
|- src/
|
|
570
|
+
| `- process_executor/
|
|
571
|
+
| |- __init__.py
|
|
572
|
+
| |- executor.py
|
|
573
|
+
| `- shared_context.py
|
|
574
|
+
|- tests/
|
|
575
|
+
| |- test_process_executor.py
|
|
576
|
+
| `- test_example.py
|
|
577
|
+
`- example/
|
|
578
|
+
|- dummy_single.py
|
|
579
|
+
|- dummy_ctrl.py
|
|
580
|
+
|- example.py
|
|
581
|
+
`- pm2cd/
|
|
582
|
+
|- single/
|
|
583
|
+
| |- client_sync.py
|
|
584
|
+
| `- server.py
|
|
585
|
+
|- multi/
|
|
586
|
+
| |- client_sync.py
|
|
587
|
+
| |- pm2cd_multi.py
|
|
588
|
+
| `- server.py
|
|
589
|
+
|- multi_shared/
|
|
590
|
+
| |- client_sync.py
|
|
591
|
+
| |- pm2cd_multi.py
|
|
592
|
+
| `- server.py
|
|
593
|
+
|- try_move_single_local.py
|
|
594
|
+
|- try_move_single_client.py
|
|
595
|
+
|- try_move_multi_local.py
|
|
596
|
+
|- try_move_multi_client.py
|
|
597
|
+
|- try_move_multi_shared_local.py
|
|
598
|
+
|- try_move_multi_shared_client.py
|
|
599
|
+
`- x1_multi/
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
`tests/` は pytest の test suite である。
|
|
603
|
+
|
|
604
|
+
`example/` は install package ではない。直接実行する sample / tutorial 用 directory である。
|
|
605
|
+
|
|
606
|
+
`example/pm2cd/x1_multi/` は v0.1.1 で置き換えた旧 PM2CD multi example の保存用 directory である。現在の example は `multi/` と `multi_shared/` を使用する。
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## 15. example 1: 自分で作ったクラスを multi 化する
|
|
611
|
+
|
|
612
|
+
対象は次である。
|
|
613
|
+
|
|
614
|
+
```text
|
|
615
|
+
example/dummy_single.py
|
|
616
|
+
example/dummy_ctrl.py
|
|
617
|
+
example/example.py
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### 15.1 `dummy_single.py`
|
|
621
|
+
|
|
622
|
+
`DummySingle` は multiprocessing を知らない通常の同期クラスである。
|
|
623
|
+
|
|
624
|
+
```python
|
|
625
|
+
class DummySingle:
|
|
626
|
+
def query(
|
|
627
|
+
self,
|
|
628
|
+
name: str,
|
|
629
|
+
) -> str:
|
|
630
|
+
return "dummy:{}".format(name)
|
|
631
|
+
|
|
632
|
+
def get(
|
|
633
|
+
self,
|
|
634
|
+
value: int,
|
|
635
|
+
delay_sec: float = 0.5,
|
|
636
|
+
) -> int:
|
|
637
|
+
time.sleep(delay_sec)
|
|
638
|
+
return value
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 15.2 `dummy_ctrl.py`
|
|
642
|
+
|
|
643
|
+
`DummyCtrl` は `ProcessExecutor` を composition する。
|
|
644
|
+
|
|
645
|
+
この example では `worker_factory` I/F を示す。
|
|
646
|
+
|
|
647
|
+
```python
|
|
648
|
+
def create_dummy_single(
|
|
649
|
+
context: SharedContext,
|
|
650
|
+
) -> DummySingle:
|
|
651
|
+
_ = context
|
|
652
|
+
return DummySingle()
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
`DummyCtrl.get()` や `DummyCtrl.query()` は結果ではなく job ID を返す。
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
job_id = self._executor.submit(
|
|
659
|
+
"get",
|
|
660
|
+
value,
|
|
661
|
+
delay_sec,
|
|
662
|
+
)
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 15.3 `example.py`
|
|
666
|
+
|
|
667
|
+
二つの worker を生成し、時間のかかる `get()` と `query()` を別 job として投入する。
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
cd example
|
|
671
|
+
python example.py
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
この example は、single class と multiprocessing facade を分離する基本形を示すものである。
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 16. example 2: 既存の制御クラスを multi 化する
|
|
679
|
+
|
|
680
|
+
PM2CD example は、single、基本 multi、shared multi を段階的に確認できる構成にしている。
|
|
681
|
+
|
|
682
|
+
```text
|
|
683
|
+
example/pm2cd/
|
|
684
|
+
|- single/
|
|
685
|
+
| |- client_sync.py
|
|
686
|
+
| `- server.py
|
|
687
|
+
|- multi/
|
|
688
|
+
| |- client_sync.py
|
|
689
|
+
| |- pm2cd_multi.py
|
|
690
|
+
| `- server.py
|
|
691
|
+
|- multi_shared/
|
|
692
|
+
| |- client_sync.py
|
|
693
|
+
| |- pm2cd_multi.py
|
|
694
|
+
| `- server.py
|
|
695
|
+
|- try_move_single_local.py
|
|
696
|
+
|- try_move_single_client.py
|
|
697
|
+
|- try_move_multi_local.py
|
|
698
|
+
|- try_move_multi_client.py
|
|
699
|
+
|- try_move_multi_shared_local.py
|
|
700
|
+
`- try_move_multi_shared_client.py
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### 16.1 single
|
|
704
|
+
|
|
705
|
+
`try_move_single_local.py` は既存の PM2CD single class を直接使用する基準実行例である。
|
|
706
|
+
|
|
707
|
+
`single/server.py` と `single/client_sync.py` は、同じ single class を FastAPI server/client 経由で確認する example である。
|
|
708
|
+
|
|
709
|
+
single class 自体を `process-executor` 用に書き換えることはしない。
|
|
710
|
+
|
|
711
|
+
### 16.2 `multi`
|
|
712
|
+
|
|
713
|
+
`multi/pm2cd_multi.py` は、複数 worker process を使用する基本 multi example である。
|
|
714
|
+
|
|
715
|
+
各 worker process では既存 `PM2CDsingle` を生成する。ただし、各 worker が独立した TCP connection を所有しないように、物理通信は manager process 側の一つの `Communication` に集約する。
|
|
716
|
+
|
|
717
|
+
概念的な構造は次である。
|
|
718
|
+
|
|
719
|
+
```text
|
|
720
|
+
PM2CDsingle worker 1 --+
|
|
721
|
+
PM2CDsingle worker 2 --+--> Communication proxy
|
|
722
|
+
PM2CDsingle worker N --+ |
|
|
723
|
+
v
|
|
724
|
+
Communication x 1
|
|
725
|
+
|
|
|
726
|
+
v
|
|
727
|
+
TCP connection x 1
|
|
728
|
+
|
|
|
729
|
+
v
|
|
730
|
+
PM2CD
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
`Communication` は `PM2CDsingle(..., communication=...)` の injection point を利用して各 worker に同じ proxy を渡す。
|
|
734
|
+
|
|
735
|
+
したがって、worker process は複数でも、PM2CD との物理通信経路は一つである。
|
|
736
|
+
|
|
737
|
+
PM2CD 専用の worker class を別に作成しない。controller logic は既存 `PM2CDsingle` を利用する。
|
|
738
|
+
|
|
739
|
+
worker 数は multi facade の constructor の `worker_count` で指定する。
|
|
740
|
+
|
|
741
|
+
example script ではコマンドラインオプションを使わず、コード内の `WORKER_COUNT` を設定値として渡す。
|
|
742
|
+
|
|
743
|
+
基本 `multi` では controller-local な移動コンテキストは共有しない。そのため、sample の `wait_for_complete()` は worker-local state に依存しないよう、`backlash=False` と明示した移動に対して `target_position` を明示して確認する。
|
|
744
|
+
|
|
745
|
+
### 16.3 `multi_shared`
|
|
746
|
+
|
|
747
|
+
`multi_shared/pm2cd_multi.py` は、`multi` の単一 Communication 構成に加えて、worker process 間で PM2CD の移動コンテキストを共有する example である。
|
|
748
|
+
|
|
749
|
+
`ProcessExecutor` の `worker_factory(context)` を使用して既存 `PM2CDsingle` を生成し、worker に渡された `SharedContext.state` を `PM2CDsingle` の `_move_context` として利用する。
|
|
750
|
+
|
|
751
|
+
```text
|
|
752
|
+
SharedContext.state
|
|
753
|
+
^
|
|
754
|
+
|
|
|
755
|
+
+----------------+----------------+
|
|
756
|
+
| | |
|
|
757
|
+
PM2CDsingle PM2CDsingle PM2CDsingle
|
|
758
|
+
worker 1 worker 2 worker N
|
|
759
|
+
\ | /
|
|
760
|
+
+------ Communication proxy -----+
|
|
761
|
+
|
|
|
762
|
+
v
|
|
763
|
+
Communication x 1
|
|
764
|
+
|
|
|
765
|
+
v
|
|
766
|
+
PM2CD
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
PM2CD の移動コンテキストには、`target_position`、backlash の有効状態、backlash value など、`move()` / `move_relative()` から `wait_for_complete()` へ引き継ぐ情報が保存される。
|
|
770
|
+
|
|
771
|
+
これを共有することで、`move()` を実行した worker と `wait_for_complete()` を実行する worker が異なっても、同じ移動コンテキストを利用できる。
|
|
772
|
+
|
|
773
|
+
共有機構は multi facade 内部の実装であり、呼び出し側に shared 専用 API を追加しない。
|
|
774
|
+
|
|
775
|
+
### 16.4 local と FastAPI server/client
|
|
776
|
+
|
|
777
|
+
各構成は local と FastAPI server/client の両方で確認できる。
|
|
778
|
+
|
|
779
|
+
```text
|
|
780
|
+
single
|
|
781
|
+
try_move_single_local.py
|
|
782
|
+
try_move_single_client.py
|
|
783
|
+
|
|
784
|
+
multi
|
|
785
|
+
try_move_multi_local.py
|
|
786
|
+
try_move_multi_client.py
|
|
787
|
+
|
|
788
|
+
multi_shared
|
|
789
|
+
try_move_multi_shared_local.py
|
|
790
|
+
try_move_multi_shared_client.py
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
multi facade の `_execute()` は内部で `submit()` と `wait()` を組み合わせるため、これら PM2CD example の public API は同期的に見える。
|
|
794
|
+
|
|
795
|
+
これは `ProcessExecutor` 自体が同期 API を強制しているのではなく、PM2CD multi facade 側の設計である。
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## 17. PM2CD で複数 worker を使用する構成
|
|
800
|
+
|
|
801
|
+
`ProcessExecutor` は worker ごとに worker-local instance を一つ生成する。
|
|
802
|
+
|
|
803
|
+
したがって `worker_count=N` なら、PM2CD example では `PM2CDsingle` も N 個生成される。
|
|
804
|
+
|
|
805
|
+
ただし v0.1.1 の PM2CD `multi` / `multi_shared` では、`PM2CDsingle` が使用する `Communication` を各 worker 内で個別生成しない。
|
|
806
|
+
|
|
807
|
+
manager process 側で一つだけ生成した `Communication` の proxy を、すべての `PM2CDsingle` に injection する。
|
|
808
|
+
|
|
809
|
+
この設計により、
|
|
810
|
+
|
|
811
|
+
```text
|
|
812
|
+
controller instance: N
|
|
813
|
+
worker process: N
|
|
814
|
+
Communication: 1
|
|
815
|
+
TCP connection: 1
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
という構成にできる。
|
|
819
|
+
|
|
820
|
+
重要なのは、この Communication 集約機能が `ProcessExecutor` core に組み込まれたわけではない点である。
|
|
821
|
+
|
|
822
|
+
`ProcessExecutor` は引き続き装置 protocol や TCP connection を知らない。PM2CD example 側が、PM2CD の dependency injection point と `BaseManager` を利用して物理 resource の ownership を構成している。
|
|
823
|
+
|
|
824
|
+
旧 example の `worker_count=1` 構成は `example/pm2cd/x1_multi/` に保存している。
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
## 18. どのようなケースに有益か
|
|
829
|
+
|
|
830
|
+
### 18.1 既存 Ctrl を変更したくない場合
|
|
831
|
+
|
|
832
|
+
すでに単体で動作している class を維持し、実行場所だけを worker process に移したい場合である。
|
|
833
|
+
|
|
834
|
+
### 18.2 multiprocessing の共通処理を装置ごとに再実装したくない場合
|
|
835
|
+
|
|
836
|
+
job queue、worker loop、job metadata、worker metadata、結果管理を共通化したい場合である。
|
|
837
|
+
|
|
838
|
+
### 18.3 上位 server と実処理を分離したい場合
|
|
839
|
+
|
|
840
|
+
FastAPI 等の上位層から job を投入し、実処理を worker process に任せる構成の基盤として利用できる。
|
|
841
|
+
|
|
842
|
+
### 18.4 job ID を中心に状態管理したい場合
|
|
843
|
+
|
|
844
|
+
```text
|
|
845
|
+
submit
|
|
846
|
+
↓
|
|
847
|
+
job ID
|
|
848
|
+
↓
|
|
849
|
+
state
|
|
850
|
+
↓
|
|
851
|
+
result / wait
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
という I/F にしたい場合である。
|
|
855
|
+
|
|
856
|
+
### 18.5 handler ごとに process を生成したくない場合
|
|
857
|
+
|
|
858
|
+
事前起動した generic worker を再利用したい場合である。
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## 19. 追加設計が必要なケース
|
|
863
|
+
|
|
864
|
+
### 19.1 一つの物理 resource を複数 worker で共有する場合
|
|
865
|
+
|
|
866
|
+
一つの TCP socket、serial port、USB handle 等を複数 worker process から利用する場合は、resource owner process と IPC proxy 等の装置側設計が必要である。
|
|
867
|
+
|
|
868
|
+
`ProcessExecutor` core 自体はその broker 機能を持たない。
|
|
869
|
+
|
|
870
|
+
v0.1.1 の PM2CD `multi` / `multi_shared` は、その具体例として、PM2CD example 側に manager-owned `Communication` を置き、同じ proxy を複数 worker の `PM2CDsingle` へ injection している。
|
|
871
|
+
|
|
872
|
+
### 19.2 thread のように同一 Python object を共有したい場合
|
|
873
|
+
|
|
874
|
+
process は address space が別である。通常の object reference を thread と同じように共有する実行器ではない。
|
|
875
|
+
|
|
876
|
+
共有が必要な controller state は、必要に応じて `SharedContext.state` と `SharedContext.lock` を利用する。
|
|
877
|
+
|
|
878
|
+
### 19.3 超低 latency の shared-memory parallelism が必要な場合
|
|
879
|
+
|
|
880
|
+
現在の実装は `multiprocessing.Manager` と `Queue` を利用する。高頻度数値計算向け shared-memory executor を目的にはしていない。
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## 20. `spawn` 使用時の条件
|
|
885
|
+
|
|
886
|
+
macOS 等で `spawn` を使用する場合、実行 script は次の形式にする。
|
|
887
|
+
|
|
888
|
+
```python
|
|
889
|
+
def main() -> None:
|
|
890
|
+
...
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
if __name__ == "__main__":
|
|
894
|
+
main()
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
worker class、worker factory、constructor 引数等は process 境界を越えて利用可能な形である必要がある。
|
|
898
|
+
|
|
899
|
+
本 repository の example はこの形式にしている。
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## 21. tests
|
|
904
|
+
|
|
905
|
+
pytest test は `tests/` に置く。
|
|
906
|
+
|
|
907
|
+
```bash
|
|
908
|
+
pytest
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
`tests/test_process_executor.py` では次を検証する。
|
|
912
|
+
|
|
913
|
+
- `submit()` が非 blocking であること。
|
|
914
|
+
- 複数 worker が独立 job を並列実行すること。
|
|
915
|
+
- `SharedContext.state` と `lock` が worker から利用できること。
|
|
916
|
+
- `result()` が暗黙に待機しないこと。
|
|
917
|
+
- worker-local handler 内の `Exception` が failed job になること。
|
|
918
|
+
- 引数と戻り値が process 境界を越えること。
|
|
919
|
+
- worker 数が維持されること。
|
|
920
|
+
- `close()` 後の `submit()` が拒否されること。
|
|
921
|
+
- `worker_class` で既存 class を直接生成できること。
|
|
922
|
+
- `worker_factory` と `worker_class` の排他条件。
|
|
923
|
+
|
|
924
|
+
`tests/test_example.py` では generic example を検証する。
|
|
925
|
+
|
|
926
|
+
PM2CD example は実機依存であるため pytest から実機操作は行わない。
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
## 22. install
|
|
931
|
+
|
|
932
|
+
repository root で次を実行する。
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
pip install -e .
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
`pyproject.toml` の version は 0.1.1 である。
|
|
939
|
+
|
|
940
|
+
Python は 3.10 以上を対象としている。
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
## 23. 最小使用例
|
|
945
|
+
|
|
946
|
+
既存 class が次であるとする。
|
|
947
|
+
|
|
948
|
+
```python
|
|
949
|
+
class MyCtrl:
|
|
950
|
+
def __init__(
|
|
951
|
+
self,
|
|
952
|
+
base: int,
|
|
953
|
+
) -> None:
|
|
954
|
+
self._base = base
|
|
955
|
+
|
|
956
|
+
def run(
|
|
957
|
+
self,
|
|
958
|
+
value: int,
|
|
959
|
+
) -> int:
|
|
960
|
+
return self._base + value
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
この class 自体を変更せず次のように実行できる。
|
|
964
|
+
|
|
965
|
+
```python
|
|
966
|
+
from process_executor import ProcessExecutor
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def main() -> None:
|
|
970
|
+
executor = ProcessExecutor(
|
|
971
|
+
worker_class=MyCtrl,
|
|
972
|
+
worker_kwargs={
|
|
973
|
+
"base": 10,
|
|
974
|
+
},
|
|
975
|
+
worker_count=2,
|
|
976
|
+
start_method="spawn",
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
try:
|
|
980
|
+
job_id = executor.submit(
|
|
981
|
+
"run",
|
|
982
|
+
5,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
result = executor.wait(
|
|
986
|
+
job_id,
|
|
987
|
+
timeout_sec=5.0,
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
print(result)
|
|
991
|
+
finally:
|
|
992
|
+
executor.close()
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
if __name__ == "__main__":
|
|
996
|
+
main()
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
結果は `15` である。
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
## 24. 現在の責務境界
|
|
1004
|
+
|
|
1005
|
+
ProcessExecutor が担当するものは次である。
|
|
1006
|
+
|
|
1007
|
+
- worker process の生成
|
|
1008
|
+
- worker-local instance の生成
|
|
1009
|
+
- job queue
|
|
1010
|
+
- job ID
|
|
1011
|
+
- job state
|
|
1012
|
+
- job result / error
|
|
1013
|
+
- worker metadata
|
|
1014
|
+
- shared state
|
|
1015
|
+
- process-safe generic lock
|
|
1016
|
+
- dead worker の検出と補充
|
|
1017
|
+
- worker lifecycle
|
|
1018
|
+
- Queue / Manager の終了
|
|
1019
|
+
|
|
1020
|
+
担当しないものは次である。
|
|
1021
|
+
|
|
1022
|
+
- 装置 protocol
|
|
1023
|
+
- TCP / serial command
|
|
1024
|
+
- 装置固有の排他規則
|
|
1025
|
+
- 物理装置の動作完了判定
|
|
1026
|
+
- 装置固有の error recovery
|
|
1027
|
+
- 単一 Communication を複数 worker で共有する装置固有 broker の実装
|
|
1028
|
+
- FastAPI / gRPC 等の外部 server
|
|
1029
|
+
- process-safe rotating log aggregation
|
|
1030
|
+
|
|
1031
|
+
この責務境界を維持することが、現在の設計上の中心である。
|