parallelazy 0.0.1__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.
- parallelazy/__init__.py +43 -0
- parallelazy/environment_manager.py +55 -0
- parallelazy/examples/__init__.py +0 -0
- parallelazy/examples/example_configuration.py +194 -0
- parallelazy/examples/example_count.py +84 -0
- parallelazy/examples/example_data_pipeline.py +226 -0
- parallelazy/examples/example_env_variables.py +158 -0
- parallelazy/examples/example_error_handling.py +266 -0
- parallelazy/examples/example_graceful_shutdown.py +139 -0
- parallelazy/examples/example_ml_training.py +280 -0
- parallelazy/examples/example_pubsub.py +137 -0
- parallelazy/examples/example_rpc_calls.py +182 -0
- parallelazy/examples/example_worker_pool.py +188 -0
- parallelazy/plugin.py +223 -0
- parallelazy/plugin_data.py +466 -0
- parallelazy/plugin_manager.py +617 -0
- parallelazy/plugin_rpc.py +511 -0
- parallelazy/plugin_transport.py +347 -0
- parallelazy/plugin_worker.py +700 -0
- parallelazy/tests/__init__.py +0 -0
- parallelazy/utils.py +71 -0
- parallelazy-0.0.1.dist-info/METADATA +17 -0
- parallelazy-0.0.1.dist-info/RECORD +26 -0
- parallelazy-0.0.1.dist-info/WHEEL +5 -0
- parallelazy-0.0.1.dist-info/licenses/LICENSE +339 -0
- parallelazy-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin Manager: Lightweight multiprocessing-based plugin orchestration.
|
|
3
|
+
|
|
4
|
+
Manages plugin lifecycle, pub/sub messaging, environment variables, and services.
|
|
5
|
+
Uses simplified request/response protocol via Channel.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import torch.multiprocessing as mp
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from parallelazy.utils.utils import process_start_spawn
|
|
17
|
+
from parallelazy.plugin.plugin_base import PluginDescriptor
|
|
18
|
+
from parallelazy.plugin.plugin_rpc import PluginRPC, RequestEnvGetAll, RequestEnvUnsubscribe, RequestTopicAnnounce
|
|
19
|
+
from parallelazy.plugin.plugin_worker import PluginWorker, Queue, RequestPluginCall, RequestTopicSubscribe, RequestTopicUnsubscribe
|
|
20
|
+
from parallelazy.plugin.plugin_data import CommandType, ProcessStatus, RequestEnvGet, RequestEnvSet, RequestEnvSubscribe, PluginStatus, Subscriber
|
|
21
|
+
from parallelazy.plugin.environment_manager import EnvironmentManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
logger.setLevel(logging.DEBUG)
|
|
26
|
+
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
process_start_spawn()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class PluginInfo:
|
|
34
|
+
plugin_id: str
|
|
35
|
+
process: mp.Process
|
|
36
|
+
rpc: PluginRPC
|
|
37
|
+
descriptor: PluginDescriptor
|
|
38
|
+
options: Dict[str, Any]
|
|
39
|
+
stop_confirmed: bool = False
|
|
40
|
+
|
|
41
|
+
class PluginManager:
|
|
42
|
+
"""
|
|
43
|
+
Lightweight manager for multiprocessing-based plugin orchestration.
|
|
44
|
+
|
|
45
|
+
Responsibilities:
|
|
46
|
+
- Plugin lifecycle (load, start, stop, unload)
|
|
47
|
+
- Pub/sub topic routing
|
|
48
|
+
- Environment variable management and subscriptions
|
|
49
|
+
- Service request/response dispatch
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
timeout_request: float = 5.0,
|
|
55
|
+
timeout_start: float = 5.0,
|
|
56
|
+
worker_pydebug: bool = None
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize plugin manager.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
timeout_request: Default timeout for sync requests (seconds)
|
|
63
|
+
timeout_start: Timeout for start command (seconds)
|
|
64
|
+
worker_pydebug: Whether to enable pydebug for worker processes (optional, for debugging plugins)
|
|
65
|
+
"""
|
|
66
|
+
self._plugins: Dict[str, PluginInfo] = {} # plugin_id -> PluginInfo
|
|
67
|
+
self._plugins_lock = threading.Lock() # For thread-safe plugin management
|
|
68
|
+
|
|
69
|
+
self._topic_publishers: Dict[str, str] = {} # topic -> plugin_id
|
|
70
|
+
self._topic_subscribers: Dict[str, Dict[str, Subscriber]] = {} # topic -> [queue, queue, ...]
|
|
71
|
+
self._env_subscribers: Dict[str, List[str]] = {} # env_key -> [plugin_id, ...]
|
|
72
|
+
|
|
73
|
+
self._env = EnvironmentManager()
|
|
74
|
+
|
|
75
|
+
self.timeout_request = timeout_request
|
|
76
|
+
self.timeout_start = timeout_start
|
|
77
|
+
self.worker_pydebug = False if worker_pydebug is None else bool(worker_pydebug)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_plugins(self, descriptors: List[PluginDescriptor], parallel: bool = True) -> None:
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
Load and initialize multiple plugins.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
descriptors: List of PluginDescriptor objects
|
|
87
|
+
parallel: Whether to load plugins in parallel
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if parallel:
|
|
91
|
+
barrier = mp.Barrier(len(descriptors) + 1) # +1 for the main process
|
|
92
|
+
for descriptor in descriptors:
|
|
93
|
+
thread = threading.Thread(target=self._load_plugin_thread, args=(descriptor, barrier))
|
|
94
|
+
thread.start()
|
|
95
|
+
barrier.wait() # Wait for all plugins to be loaded
|
|
96
|
+
else:
|
|
97
|
+
for descriptor in descriptors:
|
|
98
|
+
self.load_plugin(descriptor)
|
|
99
|
+
|
|
100
|
+
def _load_plugin_thread(self, descriptor: PluginDescriptor, barrier: 'mp.Barrier') -> None:
|
|
101
|
+
"""
|
|
102
|
+
Thread target for loading a plugin and waiting at the barrier.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
descriptor: PluginDescriptor object
|
|
106
|
+
barrier: Multiprocessing barrier for synchronization
|
|
107
|
+
"""
|
|
108
|
+
self.load_plugin(descriptor)
|
|
109
|
+
barrier.wait() # Wait for all plugins to be loaded
|
|
110
|
+
|
|
111
|
+
def load_plugin(
|
|
112
|
+
self,
|
|
113
|
+
descriptor: PluginDescriptor,
|
|
114
|
+
options: Optional[Dict[str, Any]] = None
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Load and initialize a plugin (spawn subprocess).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
descriptor: Plugin descriptor
|
|
121
|
+
options: Initial options for plugin (passed to plugin on start)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if loaded successfully
|
|
125
|
+
"""
|
|
126
|
+
plugin_id = descriptor.name
|
|
127
|
+
options = options or descriptor.options or {}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
plugin_rpc = PluginRPC(plugin_id)
|
|
131
|
+
self._init_plugin_rpc(plugin_rpc)
|
|
132
|
+
worker_channel = plugin_rpc.channel.remote_descriptor() # For worker to manager communication
|
|
133
|
+
initialized = mp.Event()
|
|
134
|
+
initialized.clear() # Ensure not set
|
|
135
|
+
|
|
136
|
+
# Spawn worker process
|
|
137
|
+
process = mp.Process(
|
|
138
|
+
target=PluginWorker.run,
|
|
139
|
+
args=(plugin_id, descriptor.to_dict(), options, worker_channel, initialized, self.worker_pydebug ),
|
|
140
|
+
name=f"plugin-{plugin_id}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
self._plugins[plugin_id] = PluginInfo(
|
|
145
|
+
plugin_id=plugin_id,
|
|
146
|
+
process=process,
|
|
147
|
+
rpc=plugin_rpc,
|
|
148
|
+
descriptor=descriptor,
|
|
149
|
+
options=options
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
process.start()
|
|
153
|
+
initialized.wait(timeout=self.timeout_start)
|
|
154
|
+
|
|
155
|
+
logger.info(f"Plugin {plugin_id} loaded")
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"Error loading plugin {descriptor}: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def start_plugin(self, plugin_id: str) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Start a loaded plugin and register subscriptions.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
plugin_id: Plugin identifier
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if started successfully
|
|
171
|
+
"""
|
|
172
|
+
if plugin_id not in self._plugins:
|
|
173
|
+
logger.error(f"Plugin {plugin_id} not loaded")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
info = self._plugins[plugin_id]
|
|
178
|
+
pending = info.rpc.plugin_start()
|
|
179
|
+
res = pending.wait(timeout=self.timeout_start)
|
|
180
|
+
if not res:
|
|
181
|
+
logger.warning(f"Plugin {plugin_id} failed to start within {self.timeout_start} seconds")
|
|
182
|
+
return False
|
|
183
|
+
if res.error:
|
|
184
|
+
logger.error(f"Plugin {plugin_id} start error: {res.error}")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return res.result
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Error starting plugin {plugin_id}: {e}")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def stop_plugin(self, plugin_id: str) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Stop a running plugin.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
plugin_id: Plugin identifier
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if stopped successfully
|
|
202
|
+
"""
|
|
203
|
+
if plugin_id not in self._plugins:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
info = self._plugins[plugin_id]
|
|
208
|
+
if not info.process.is_alive(): return True # Already stopped
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
pending = info.rpc.plugin_stop()
|
|
212
|
+
res = pending.wait(timeout=self.timeout_start)
|
|
213
|
+
if not res:
|
|
214
|
+
logger.warning(f"Plugin {plugin_id} failed to stop within {self.timeout_start} seconds")
|
|
215
|
+
return False
|
|
216
|
+
if res.error:
|
|
217
|
+
logger.error(f"Plugin {plugin_id} stop error: {res.error}")
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
logger.info(f"Plugin {plugin_id} stop: {res.result}")
|
|
221
|
+
if res.result:
|
|
222
|
+
info.stop_confirmed = True
|
|
223
|
+
return res.result
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Error stopping plugin {plugin_id}: {e}")
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def unload_plugin(self, plugin_id: str) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Unload a plugin and terminate its process.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
plugin_id: Plugin identifier
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if unloaded successfully
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
if plugin_id in self._plugins:
|
|
240
|
+
info = self._plugins[plugin_id]
|
|
241
|
+
if info.process.is_alive() and not info.stop_confirmed:
|
|
242
|
+
self.stop_plugin(plugin_id)
|
|
243
|
+
|
|
244
|
+
if plugin_id in self._plugins:
|
|
245
|
+
info = self._plugins[plugin_id]
|
|
246
|
+
process = info.process
|
|
247
|
+
|
|
248
|
+
info.rpc.shutdown() # Clean up RPC resources
|
|
249
|
+
|
|
250
|
+
process.join(timeout=self.timeout_start)
|
|
251
|
+
|
|
252
|
+
if process.is_alive():
|
|
253
|
+
process.terminate()
|
|
254
|
+
process.join(timeout=self.timeout_start)
|
|
255
|
+
|
|
256
|
+
if process.is_alive():
|
|
257
|
+
process.kill()
|
|
258
|
+
|
|
259
|
+
# Clean up environment subscriptions
|
|
260
|
+
for env, plugins in list(self._env_subscribers.items()):
|
|
261
|
+
if plugin_id in plugins:
|
|
262
|
+
plugins.remove(plugin_id)
|
|
263
|
+
if len(plugins) == 0:
|
|
264
|
+
del self._env_subscribers[env]
|
|
265
|
+
|
|
266
|
+
# Clean up topic subscriptions
|
|
267
|
+
for topic, plugins in list(self._topic_subscribers.items()):
|
|
268
|
+
if plugin_id in plugins.keys():
|
|
269
|
+
del plugins[plugin_id]
|
|
270
|
+
|
|
271
|
+
# Clean up topic publishers
|
|
272
|
+
for topic, publisher in list(self._topic_publishers.items()):
|
|
273
|
+
if publisher == plugin_id:
|
|
274
|
+
del self._topic_publishers[topic]
|
|
275
|
+
|
|
276
|
+
del self._plugins[plugin_id]
|
|
277
|
+
|
|
278
|
+
logger.info(f"Plugin {plugin_id} unloaded")
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
import traceback
|
|
283
|
+
dump = traceback.print_exc()
|
|
284
|
+
logger.error(f"Error unloading plugin {plugin_id}: {e}\n{dump}", stack_info=True)
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
def start_all_plugins(self) -> None:
|
|
288
|
+
"""Start all loaded plugins."""
|
|
289
|
+
for plugin_id in list(self._plugins.keys()):
|
|
290
|
+
self.start_plugin(plugin_id)
|
|
291
|
+
|
|
292
|
+
def stop_all_plugins(self) -> None:
|
|
293
|
+
"""Stop all running plugins."""
|
|
294
|
+
for plugin_id in list(self._plugins.keys()):
|
|
295
|
+
self.stop_plugin(plugin_id)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def unload_all_plugins(self) -> None:
|
|
299
|
+
"""Shut down the plugin manager and all plugins."""
|
|
300
|
+
self.stop_all_plugins()
|
|
301
|
+
for plugin_id in list(self._plugins.keys()):
|
|
302
|
+
self.unload_plugin(plugin_id)
|
|
303
|
+
|
|
304
|
+
# ========================================================================
|
|
305
|
+
# Utility Methods for Plugin Management
|
|
306
|
+
# ========================================================================
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def plugin_status(self, plugin_id: str) -> Optional[ProcessStatus]:
|
|
311
|
+
"""
|
|
312
|
+
Get status of a plugin.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
plugin_id: Plugin identifier
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
ProcessStatus object, or None if not found
|
|
319
|
+
"""
|
|
320
|
+
if plugin_id not in self._plugins:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
info = self._plugins[plugin_id]
|
|
324
|
+
process = info.process
|
|
325
|
+
alive = process.is_alive()
|
|
326
|
+
pid = process.pid
|
|
327
|
+
worker_status = None
|
|
328
|
+
|
|
329
|
+
if alive:
|
|
330
|
+
try:
|
|
331
|
+
pending = info.rpc.plugin_status()
|
|
332
|
+
status_response = pending.wait(timeout=1.0)
|
|
333
|
+
if status_response is None:
|
|
334
|
+
logger.debug(f"[{plugin_id}] No status received")
|
|
335
|
+
elif status_response.error:
|
|
336
|
+
logger.debug(f"[{plugin_id}] Error getting status: {status_response.error}")
|
|
337
|
+
else:
|
|
338
|
+
worker_status = status_response.result
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.debug(f"[{plugin_id}] Failed to get status: {e}")
|
|
341
|
+
|
|
342
|
+
return ProcessStatus(
|
|
343
|
+
plugin_id=plugin_id,
|
|
344
|
+
process_alive=alive,
|
|
345
|
+
process_pid=pid,
|
|
346
|
+
worker_status=worker_status
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def plugin_status_all(self) -> Dict[str, ProcessStatus]:
|
|
350
|
+
"""
|
|
351
|
+
Get status of all plugins.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Dictionary of plugin_id -> ProcessStatus
|
|
355
|
+
"""
|
|
356
|
+
return {
|
|
357
|
+
plugin_id: self.plugin_status(plugin_id)
|
|
358
|
+
for plugin_id in self._plugins.keys()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def plugin_list(self) -> List[str]:
|
|
362
|
+
"""List all loaded plugin IDs."""
|
|
363
|
+
return list(self._plugins.keys())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ========================================================================
|
|
370
|
+
# Environment Variable Management
|
|
371
|
+
# ========================================================================
|
|
372
|
+
|
|
373
|
+
def env_set(self, name:str, value:Any) -> None:
|
|
374
|
+
self._env.set(name, value)
|
|
375
|
+
threading.Thread(target=self._notify_env_update, args=(name,), daemon=True).start()
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
def _notify_env_update(self, name:str) -> None:
|
|
379
|
+
# Notify subscribed plugins
|
|
380
|
+
if name in self._env_subscribers:
|
|
381
|
+
for plugin_id in self._env_subscribers[name]:
|
|
382
|
+
if plugin_id in self._plugins:
|
|
383
|
+
rpc = self._plugins[plugin_id].rpc
|
|
384
|
+
rpc.env_update(name)
|
|
385
|
+
|
|
386
|
+
def env_get(self, name:str=None, default:Any=None) -> Any:
|
|
387
|
+
if name is None:
|
|
388
|
+
return self._env.get_all()
|
|
389
|
+
return self._env.get(name, default)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def env_subscribe(self, plugin_id: str, var_name: str) -> bool:
|
|
393
|
+
if plugin_id not in self._plugins:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
if var_name not in self._env_subscribers:
|
|
397
|
+
self._env_subscribers[var_name] = []
|
|
398
|
+
|
|
399
|
+
if plugin_id not in self._env_subscribers[var_name]:
|
|
400
|
+
self._env_subscribers[var_name].append(plugin_id)
|
|
401
|
+
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
def env_unsubscribe(self, plugin_id: str, var_name: str) -> bool:
|
|
405
|
+
if plugin_id not in self._plugins:
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
if var_name in self._env_subscribers and plugin_id in self._env_subscribers[var_name]:
|
|
409
|
+
self._env_subscribers[var_name].remove(plugin_id)
|
|
410
|
+
if not self._env_subscribers[var_name]:
|
|
411
|
+
del self._env_subscribers[var_name]
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
# ========================================================================
|
|
415
|
+
# Service Management (Optional, for future extension)
|
|
416
|
+
# ========================================================================
|
|
417
|
+
|
|
418
|
+
def topic_publish(self, topic: str, data: Any):
|
|
419
|
+
"""
|
|
420
|
+
Publish data to a topic.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
topic: Topic name
|
|
424
|
+
data: Message data
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Number of subscribers that received the message
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
if topic not in self._topic_subscribers: return True
|
|
431
|
+
|
|
432
|
+
for subscriber in self._topic_subscribers[topic].values():
|
|
433
|
+
try:
|
|
434
|
+
subscriber.queue.put_nowait(data)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.debug(f"Failed to deliver to topic '{topic}': {e}")
|
|
437
|
+
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
def topic_list(self) -> List[str]:
|
|
441
|
+
return list(self._topic_subscribers.keys())
|
|
442
|
+
|
|
443
|
+
# ========================================================================
|
|
444
|
+
# PLUGIN Lifecycle
|
|
445
|
+
# ========================================================================
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ========================================================================
|
|
450
|
+
# RPC Responder: Handle requests from plugins
|
|
451
|
+
# ========================================================================
|
|
452
|
+
|
|
453
|
+
def _init_plugin_rpc(self, plugin_rpc: PluginRPC) -> None:
|
|
454
|
+
"""Initialize RPC callbacks for a master.
|
|
455
|
+
|
|
456
|
+
#PLUGIN_START = "plugin_start"
|
|
457
|
+
#PLUGIN_STOP = "plugin_stop"
|
|
458
|
+
#PLUGIN_STATUS = "plugin_status"
|
|
459
|
+
#PLUGIN_STATUS_ALL = "plugin_status_all"
|
|
460
|
+
#PLUGIN_ERRORS = "plugin_errors"
|
|
461
|
+
PLUGIN_CALL = "plugin_call"
|
|
462
|
+
|
|
463
|
+
ENV_SET = "env_set"
|
|
464
|
+
ENV_GET = "env_get"
|
|
465
|
+
ENV_GET_ALL = "env_get_all"
|
|
466
|
+
ENV_SUBSCRIBE = "env_subscribe"
|
|
467
|
+
ENV_UNSUBSCRIBE = "env_unsubscribe"
|
|
468
|
+
#ENV_UPDATE = "env_update"
|
|
469
|
+
|
|
470
|
+
TOPIC_SUBSCRIBE = "topic_subscribe"
|
|
471
|
+
TOPIC_UNSUBSCRIBE = "topic_unsubscribe"
|
|
472
|
+
TOPIC_PUBLISH = "topic_publish"
|
|
473
|
+
TOPIC_LIST = "topic_list"
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
#TODO: Handy but later
|
|
477
|
+
#plugin_rpc.on_request(CommandType.PLUGIN_START, self._handle_plugin_start)
|
|
478
|
+
#plugin_rpc.on_request(CommandType.PLUGIN_STOP, self._handle_plugin_stop)
|
|
479
|
+
plugin_rpc.on_request(CommandType.PLUGIN_CALL, self._respond_call) # forward
|
|
480
|
+
#plugin_rpc.on_request(CommandType.PLUGIN_STATUS, self.get_plugin_status)
|
|
481
|
+
#plugin_rpc.on_request(CommandType.PLUGIN_STATUS_ALL, self.get_all_plugin_status)
|
|
482
|
+
|
|
483
|
+
plugin_rpc.on_request(CommandType.ENV_SET, self._respond_env_set)
|
|
484
|
+
plugin_rpc.on_request(CommandType.ENV_GET, self._respond_env_get)
|
|
485
|
+
plugin_rpc.on_request(CommandType.ENV_GET_ALL, self._respond_env_get_all)
|
|
486
|
+
plugin_rpc.on_request(CommandType.ENV_SUBSCRIBE, self._respond_env_subscribe)
|
|
487
|
+
plugin_rpc.on_request(CommandType.ENV_UNSUBSCRIBE, self._respond_env_unsubscribe)
|
|
488
|
+
|
|
489
|
+
plugin_rpc.on_request(CommandType.TOPIC_SUBSCRIBE, self._respond_topic_subscribe)
|
|
490
|
+
plugin_rpc.on_request(CommandType.TOPIC_UNSUBSCRIBE, self._respond_topic_unsubscribe)
|
|
491
|
+
plugin_rpc.on_request(CommandType.TOPIC_ANNOUNCE, self._respond_topic_announce)
|
|
492
|
+
plugin_rpc.on_request(CommandType.TOPIC_LIST, self._respond_topic_list)
|
|
493
|
+
|
|
494
|
+
# ========================================================================
|
|
495
|
+
# LIFECYCLE:
|
|
496
|
+
# ========================================================================
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _respond_call(self, rpc: PluginRPC, req: RequestPluginCall) -> Any:
|
|
501
|
+
|
|
502
|
+
if req.plugin_id not in self._plugins:
|
|
503
|
+
raise ValueError(f"Plugin {req.plugin_id} not found")
|
|
504
|
+
|
|
505
|
+
dst_rpc = self._plugins[req.plugin_id].rpc
|
|
506
|
+
remote_result = dst_rpc.forward(req)
|
|
507
|
+
|
|
508
|
+
return remote_result
|
|
509
|
+
|
|
510
|
+
# ========================================================================
|
|
511
|
+
# ENVIRONMENT: Variables and Subscriptions
|
|
512
|
+
# ========================================================================
|
|
513
|
+
|
|
514
|
+
def _respond_env_set(self, rpc: PluginRPC, req: RequestEnvSet) -> None:
|
|
515
|
+
return self.env_set(req.name, req.value)
|
|
516
|
+
|
|
517
|
+
def _respond_env_get(self, rpc: PluginRPC, req: RequestEnvGet) -> Any:
|
|
518
|
+
return self.env_get(req.name)
|
|
519
|
+
|
|
520
|
+
def _respond_env_get_all(self, rpc: PluginRPC, req: RequestEnvGetAll) -> Dict[str, Any]:
|
|
521
|
+
return self.env_get()
|
|
522
|
+
|
|
523
|
+
def _respond_env_subscribe(self, rpc: PluginRPC, req: RequestEnvSubscribe) -> bool:
|
|
524
|
+
return self.env_subscribe(req.sender, req.name)
|
|
525
|
+
|
|
526
|
+
def _respond_env_unsubscribe(self, rpc: PluginRPC, req: RequestEnvUnsubscribe) -> bool:
|
|
527
|
+
return self.env_unsubscribe(req.sender, req.name)
|
|
528
|
+
|
|
529
|
+
# ========================================================================
|
|
530
|
+
# TOPIC PUB/SUB
|
|
531
|
+
# ========================================================================
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _respond_topic_subscribe(self, rpc: PluginRPC, req: RequestTopicSubscribe) -> bool:
|
|
535
|
+
"""
|
|
536
|
+
Subscribe a plugin to a topic.
|
|
537
|
+
Allow subscription also for non (yet, hopefully) existing topics.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
sub = req.subscriber
|
|
541
|
+
topic = sub.topic
|
|
542
|
+
plugin_sub = req.sender
|
|
543
|
+
|
|
544
|
+
if plugin_sub not in self._plugins: return False
|
|
545
|
+
|
|
546
|
+
plugin_pub = self._topic_publishers.get(topic,None)
|
|
547
|
+
if plugin_pub is not None and plugin_pub not in self._plugins: return False
|
|
548
|
+
|
|
549
|
+
if topic not in self._topic_subscribers:
|
|
550
|
+
self._topic_subscribers[topic] = {}
|
|
551
|
+
|
|
552
|
+
if plugin_sub not in self._topic_subscribers[topic]:
|
|
553
|
+
self._topic_subscribers[topic][plugin_sub] = sub
|
|
554
|
+
|
|
555
|
+
remote_result = True # Default to success
|
|
556
|
+
|
|
557
|
+
if plugin_pub is not None:
|
|
558
|
+
forward_rpc = self._plugins[plugin_pub].rpc
|
|
559
|
+
remote_result = forward_rpc.forward(req)
|
|
560
|
+
logger.info("MANAGER: topic_subscribe: remote_result: %s", remote_result)
|
|
561
|
+
|
|
562
|
+
if not remote_result: return False
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _respond_topic_unsubscribe(self, rpc: PluginRPC, req: RequestTopicUnsubscribe) -> bool:
|
|
570
|
+
"""
|
|
571
|
+
Unsubscribe a plugin from a topic.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
plugin_id: Plugin identifier
|
|
575
|
+
topic: Topic name
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
True if unsubscription successful
|
|
579
|
+
"""
|
|
580
|
+
sub = req.subscriber
|
|
581
|
+
topic = sub.topic
|
|
582
|
+
src_id = sub.plugin_id
|
|
583
|
+
|
|
584
|
+
if src_id not in self._plugins: return False
|
|
585
|
+
if topic not in self._topic_publishers: return False
|
|
586
|
+
|
|
587
|
+
dst_id = self._topic_publishers[topic]
|
|
588
|
+
|
|
589
|
+
forward_rpc = self._plugins[dst_id].rpc
|
|
590
|
+
remote_result = forward_rpc.forward(req)
|
|
591
|
+
|
|
592
|
+
if not remote_result: return False
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
if src_id in self._topic_subscribers[topic]:
|
|
596
|
+
del self._topic_subscribers[topic][src_id]
|
|
597
|
+
if not self._topic_subscribers[topic]:
|
|
598
|
+
del self._topic_subscribers[topic]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
return True
|
|
602
|
+
|
|
603
|
+
def _respond_topic_announce(self, rpc: PluginRPC, req: RequestTopicAnnounce) -> Dict[str, Subscriber]:
|
|
604
|
+
"""
|
|
605
|
+
Announce a new topic.
|
|
606
|
+
|
|
607
|
+
"""
|
|
608
|
+
self._topic_publishers[req.topic] = req.sender
|
|
609
|
+
|
|
610
|
+
if req.topic not in self._topic_subscribers:
|
|
611
|
+
self._topic_subscribers[req.topic] = {}
|
|
612
|
+
subs = self._topic_subscribers[req.topic]
|
|
613
|
+
|
|
614
|
+
return subs
|
|
615
|
+
|
|
616
|
+
def _respond_topic_list(self) -> List[str]:
|
|
617
|
+
return self.topic_list()
|