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.
@@ -0,0 +1,466 @@
1
+ """
2
+ Plugin System Message Protocol (Refactored)
3
+
4
+ Unified, lean messaging system for inter-process communication:
5
+
6
+ Core Concepts:
7
+ 1. Request/Response: Unified for ALL operations (lifecycle, services, status)
8
+ 2. DataExchange: Unified subscription model for pub/sub + env vars
9
+ 3. Channel: Proper async request/response routing with background thread
10
+
11
+ Message Flows:
12
+ - Control: Manager sends Request("start"), Worker replies with Response
13
+ - Services: Manager sends Request("call_service", ...), Worker replies
14
+ - Pub/Sub: Manager.publish(topic, data) → direct to subscriber queues
15
+ - Env Vars: Manager.set_env(key, value) → broadcasts EnvUpdate to subscribers
16
+
17
+ No Commands/Responses wrappers, no double indirection. Just Request and Response.
18
+ """
19
+ from typing import Any, Callable, Optional, TYPE_CHECKING
20
+ from enum import Enum
21
+ import logging
22
+ from concurrent.futures import Future
23
+ from torch import multiprocessing as mp
24
+ from dataclasses import dataclass, field, fields
25
+ from multiprocessing.connection import Connection
26
+
27
+ import threading
28
+
29
+
30
+
31
+ if TYPE_CHECKING:
32
+ from parallelazy.plugin.plugin_worker import PluginRPC
33
+
34
+
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ RequestCallback = Callable[["PluginRPC","Request"], Any]
40
+ ResponseCallback = Callable[["PluginRPC","Response"], Any]
41
+
42
+
43
+
44
+ class QueueDescriptor:
45
+ """Descriptor for holding 2 connections, read/write repesenting a queue made with pipes."""
46
+ def __init__(self, send: Connection, recieve: Connection):
47
+ self.send = send
48
+ self.receive = recieve
49
+
50
+ def __str__(self): return f"QueueDescriptor(send_closed={self.send.closed()}, receive_closed={self.receive.closed()})"
51
+
52
+
53
+
54
+ # ============================================================================
55
+ # CHANNEL: Bidirectional duplex IPC with proper async response routing
56
+ # ============================================================================
57
+ class Queue:
58
+
59
+ def __init__(self, descriptor: QueueDescriptor = None):
60
+ if descriptor is None:
61
+ receive, send = mp.Pipe(duplex=False)
62
+ else:
63
+ receive, send = descriptor.receive, descriptor.send
64
+
65
+ self._send = QueueSend(send)
66
+ self._receive = QueueRecive(receive)
67
+
68
+ @property
69
+ def sender(self):
70
+ return self._send
71
+
72
+ @property
73
+ def receiver(self):
74
+ return self._receive
75
+
76
+ def closed(self):
77
+ return self._send._conn.closed or self._receive._conn.closed
78
+
79
+ def put(self, item):
80
+ self._send.put(item)
81
+
82
+ def put_nowait(self, item):
83
+ self._send.put(item)
84
+
85
+ def get(self):
86
+ return self._receive.get()
87
+
88
+ def get_nowait(self):
89
+ return self._receive.get()
90
+
91
+ def close(self):
92
+ self._send.put(None) # Send a sentinel to indicate closure
93
+ self._send.close()
94
+ self._receive.close()
95
+
96
+ def descriptor(self):
97
+ return QueueDescriptor(self._send._conn, self._receive._conn)
98
+
99
+
100
+ class QueueSend:
101
+ def __init__(self, conn: Connection):
102
+ self._conn = conn
103
+ self._lock = threading.Lock()
104
+
105
+ def put(self, item):
106
+ with self._lock:
107
+ self._conn.send(item)
108
+
109
+ def close(self):
110
+ self._conn.close()
111
+
112
+
113
+ class QueueRecive:
114
+ def __init__(self, conn: Connection):
115
+ self._conn = conn
116
+
117
+ def get(self):
118
+ return self._conn.recv()
119
+
120
+ def close(self):
121
+ self._conn.close()
122
+
123
+ class Channel:
124
+ """
125
+ Duplex communication channel with request/response routing.
126
+ Pass channel.reverse() to the other process — it's picklable.
127
+ """
128
+
129
+ def __init__(self, descriptor: QueueDescriptor = None ):
130
+ if descriptor is None:
131
+ r1, w1 = mp.Pipe(duplex=False)
132
+ r2, w2 = mp.Pipe(duplex=False)
133
+ self._send = QueueSend(w1)
134
+ self._receive = QueueRecive(r2)
135
+ self._remote_send = QueueSend(w2)
136
+ self._remote_receive = QueueRecive(r1)
137
+ else:
138
+ self._send = QueueSend(descriptor.send)
139
+ self._receive = QueueRecive(descriptor.receive)
140
+ self._remote_send = None
141
+ self._remote_receive = None
142
+
143
+
144
+ def descriptor(self):
145
+ return QueueDescriptor(self._send._conn, self._receive._conn)
146
+
147
+
148
+ def remote_descriptor(self):
149
+ return QueueDescriptor(self._remote_send._conn, self._remote_receive._conn)
150
+
151
+
152
+ def send(self, message) -> None:
153
+ self._send.put(message)
154
+
155
+ def receive(self) -> Optional[object]:
156
+ """Receive a message, blocking until one arrives or the pipe closes."""
157
+ return self._receive.get()
158
+
159
+ def shutdown(self):
160
+ if self._send:
161
+ self._send.close()
162
+ if self._receive:
163
+ self._receive.close()
164
+ if self._remote_send:
165
+ self._remote_send.close()
166
+ if self._remote_receive:
167
+ self._remote_receive.close()
168
+
169
+
170
+ # ============================================================================
171
+ # CORE MESSAGE TYPES
172
+ # ============================================================================
173
+
174
+
175
+ class CommandType:
176
+ """Standard commands for plugin control."""
177
+ PLUGIN_START = "plugin_start"
178
+ PLUGIN_STOP = "plugin_stop"
179
+ PLUGIN_STATUS = "plugin_status"
180
+ PLUGIN_STATUS_ALL = "plugin_status_all"
181
+ PLUGIN_ERRORS = "plugin_errors"
182
+ PLUGIN_CALL = "plugin_call"
183
+
184
+ ENV_SET = "env_set"
185
+ ENV_GET = "env_get"
186
+ ENV_GET_ALL = "env_get_all"
187
+ ENV_SUBSCRIBE = "env_subscribe"
188
+ ENV_UNSUBSCRIBE = "env_unsubscribe"
189
+ ENV_UPDATE = "env_update"
190
+
191
+ TOPIC_SUBSCRIBE = "topic_subscribe"
192
+ TOPIC_UNSUBSCRIBE = "topic_unsubscribe"
193
+ TOPIC_ANNOUNCE = "topic_announce"
194
+ TOPIC_LIST = "topic_list"
195
+
196
+ @classmethod
197
+ def commands(cls):
198
+ return iter([
199
+ cls.PLUGIN_START,
200
+ cls.PLUGIN_STOP,
201
+ cls.PLUGIN_STATUS,
202
+ cls.PLUGIN_STATUS_ALL,
203
+ cls.PLUGIN_ERRORS,
204
+ cls.PLUGIN_CALL,
205
+ cls.ENV_SET,
206
+ cls.ENV_GET,
207
+ cls.ENV_GET_ALL,
208
+ cls.ENV_SUBSCRIBE,
209
+ cls.ENV_UNSUBSCRIBE,
210
+ cls.ENV_UPDATE,
211
+ cls.TOPIC_SUBSCRIBE,
212
+ cls.TOPIC_UNSUBSCRIBE,
213
+ cls.TOPIC_ANNOUNCE,
214
+ cls.TOPIC_LIST
215
+ ])
216
+
217
+
218
+ @dataclass
219
+ class Message:
220
+ """Base message class for all communication."""
221
+ sender: str = None
222
+ message_id: str = None
223
+
224
+ def copy(self):
225
+ kwargs = {f.name: getattr(self, f.name) for f in fields(self)}
226
+ kwargs['message_id'] = None
227
+ return type(self)(**kwargs)
228
+
229
+ @dataclass
230
+ class Request(Message):
231
+ """
232
+ Unified request for all operations.ada
233
+ Worker replies with Response(message_id, result=...) or Response(message_id, error=...)
234
+ """
235
+ command: CommandType = None
236
+
237
+ def copy(self):
238
+ """Create a copy of the message (for forwarding/modification)."""
239
+ clone = type(self)(**self.__dict__)
240
+ clone.message_id = None # message_id are generated at request time.
241
+ return clone
242
+
243
+ @dataclass
244
+ class Response(Message):
245
+ """
246
+ Unified response for all request results.
247
+ - result: The return value of the request (if successful)
248
+ - error: An error message (if an error occurred)
249
+ """
250
+ result: Any = None
251
+ error: Optional[str] = None
252
+
253
+ class PendingRequest():
254
+ def __init__(self, message_id: str, callback: Callable=None):
255
+ super().__init__()
256
+ self.future = Future()
257
+ self.message_id = message_id
258
+ if callback:
259
+ self.future.add_done_callback(callback)
260
+
261
+ def on_ready(self, callback: Callable):
262
+ self.future.add_done_callback(callback)
263
+
264
+ def set_result(self, result: Response|Any):
265
+ if not isinstance(result, Response):
266
+ result = Response(message_id=self.message_id, result=result)
267
+ self.future.set_result(result)
268
+
269
+ def set_error(self, error: str|Exception|Any):
270
+ self.future.set_result(Response(message_id=self.message_id, error=str(error)))
271
+
272
+ def wait(self, timeout=None) -> None | Response:
273
+ try:
274
+ result: Response = self.future.result(timeout=timeout)
275
+ return result
276
+ except TimeoutError:
277
+ return None
278
+
279
+ def __hash__(self):
280
+ return hash(self.message_id)
281
+
282
+
283
+ class Subscription:
284
+ """
285
+ Represents a subscription to a topic or environment variable.
286
+ """
287
+ def __init__(self, topic: str, callback: Callable, queue: Queue=None, thread: Optional[threading.Thread] = None):
288
+ self.topic = topic
289
+ self.callback = callback
290
+ self.queue = queue or Queue() # For topic subscriptions, queue to receive data
291
+ self.thread: Optional[threading.Thread] = thread # Thread handling this subscription (if needed)
292
+
293
+ class Subscriber:
294
+ """Represents a subscriber to a topic or environment variable."""
295
+ def __init__(self, plugin_id: str, topic: str, queue_descriptor: QueueDescriptor, queue: Optional[Queue] = None):
296
+ self.plugin_id: str = plugin_id
297
+ self.topic: str = topic
298
+ self.queue_descriptor: QueueDescriptor = queue_descriptor
299
+ self._queue: Optional[Queue] = queue # Will be initialized when subscription is active
300
+
301
+ @property
302
+ def queue(self) -> Queue:
303
+ if self._queue is None:
304
+ self._queue = Queue(self.queue_descriptor)
305
+ return self._queue
306
+
307
+ @queue.setter
308
+ def queue(self, value: Queue):
309
+ self._queue = value
310
+
311
+
312
+ def __str__(self):
313
+ return f"Subscriber(plugin_id={self.plugin_id}, topic={self.topic}, queue_descriptor={self.queue_descriptor})"
314
+
315
+ # ============================================================================
316
+ # STATUS DATA STRUCTURES
317
+ # ============================================================================
318
+
319
+ @dataclass
320
+ class PluginStatus:
321
+ """Status snapshot from plugin worker."""
322
+ plugin_id: str
323
+ state: str # "initialized", "loaded", "running", "stopped", "error"
324
+ is_running: bool
325
+ subscriptions: int # Number of data exchange subscriptions
326
+ subscribers: int # Number of subscribers to this plugin's topics
327
+ published: int # Number of topics published
328
+ errors: int
329
+
330
+ def to_dict(self):
331
+ return {
332
+ "plugin_id": self.plugin_id,
333
+ "state": self.state,
334
+ "is_running": self.is_running,
335
+ "subscriptions": self.subscriptions,
336
+ "subscribers": self.subscribers,
337
+ "published": self.published,
338
+ "errors": self.errors
339
+ }
340
+
341
+
342
+ @dataclass
343
+ class ProcessStatus:
344
+ """Status of a plugin from manager perspective."""
345
+ plugin_id: str
346
+ process_alive: bool
347
+ process_pid: Optional[int]
348
+ worker_status: Optional[PluginStatus] = None
349
+
350
+
351
+
352
+ """
353
+ Classes for each request command:
354
+ PLUGIN_START = "plugin_start"
355
+ PLUGIN_STOP = "plugin_stop"
356
+ PLUGIN_STATUS = "plugin_status"
357
+ PLUGIN_STATUS_ALL = "plugin_status_all"
358
+ PLUGIN_ERRORS = "plugin_errors"
359
+ PLUGIN_CALL = "plugin_call"
360
+
361
+ ENV_SET = "env_set"
362
+ ENV_GET = "env_get"
363
+ ENV_GET_ALL = "env_get_all"
364
+ ENV_SUBSCRIBE = "env_subscribe"
365
+ ENV_UNSUBSCRIBE = "env_unsubscribe"
366
+ ENV_UPDATE = "env_update"
367
+
368
+ TOPIC_SUBSCRIBE = "topic_subscribe"
369
+ TOPIC_UNSUBSCRIBE = "topic_unsubscribe"
370
+ TOPIC_PUBLISH = "topic_publish"
371
+ TOPIC_LIST = "topic_list"
372
+
373
+ """
374
+
375
+
376
+
377
+ ### Plugin Lifecycle Requests
378
+
379
+ @dataclass
380
+ class RequestPluginStart(Request):
381
+ command: CommandType = field(default=CommandType.PLUGIN_START, init=False)
382
+ plugin_id: Optional[str] = None
383
+
384
+ @dataclass
385
+ class RequestPluginStop(Request):
386
+ command: CommandType = field(default=CommandType.PLUGIN_STOP, init=False)
387
+ plugin_id: Optional[str] = None
388
+
389
+ @dataclass
390
+ class RequestPluginStatus(Request):
391
+ command: CommandType = field(default=CommandType.PLUGIN_STATUS, init=False)
392
+ plugin_id: Optional[str] = None
393
+
394
+ @dataclass
395
+ class RequestPluginStatusAll(Request):
396
+ command: CommandType = field(default=CommandType.PLUGIN_STATUS_ALL, init=False)
397
+
398
+ @dataclass
399
+ class RequestPluginErrors(Request):
400
+ command: CommandType = field(default=CommandType.PLUGIN_ERRORS, init=False)
401
+ plugin_id: Optional[str] = None
402
+
403
+ @dataclass
404
+ class RequestPluginCall(Request):
405
+ command: CommandType = field(default=CommandType.PLUGIN_CALL, init=False)
406
+ plugin_id: str = None
407
+ method_name: str = None
408
+ args: tuple = None
409
+ kwargs: dict = None
410
+
411
+ @dataclass
412
+ class RequestEnvSet(Request):
413
+ command: CommandType = field(default=CommandType.ENV_SET, init=False)
414
+ name: str = None
415
+ value: Any = None
416
+
417
+ @dataclass
418
+ class RequestEnvGet(Request):
419
+ command: CommandType = field(default=CommandType.ENV_GET, init=False)
420
+ name: str = None
421
+
422
+ @dataclass
423
+ class RequestEnvGetAll(Request):
424
+ command: CommandType = field(default=CommandType.ENV_GET_ALL, init=False)
425
+
426
+
427
+ @dataclass
428
+ class RequestEnvSubscribe(Request):
429
+ command: CommandType = field(default=CommandType.ENV_SUBSCRIBE, init=False)
430
+ name: str = None
431
+
432
+
433
+ @dataclass
434
+ class RequestEnvUnsubscribe(Request):
435
+ command: CommandType = field(default=CommandType.ENV_UNSUBSCRIBE, init=False)
436
+ name: str = None
437
+
438
+ @dataclass
439
+ class RequestEnvUpdate(Request):
440
+ command: CommandType = field(default=CommandType.ENV_UPDATE, init=False)
441
+ name: str = None
442
+
443
+ ### Pub/Sub Requests
444
+
445
+ @dataclass
446
+ class RequestTopicSubscribe(Request):
447
+ command: CommandType = field(default=CommandType.TOPIC_SUBSCRIBE, init=False)
448
+ subscriber: Subscriber = None
449
+
450
+ @dataclass
451
+ class RequestTopicUnsubscribe(Request):
452
+ command: CommandType = field(default=CommandType.TOPIC_UNSUBSCRIBE, init=False)
453
+ subscriber: Subscriber = None
454
+
455
+
456
+ @dataclass
457
+ class RequestTopicAnnounce(Request):
458
+ command: CommandType = field(default=CommandType.TOPIC_ANNOUNCE, init=False)
459
+ topic: str = None
460
+
461
+
462
+ @dataclass
463
+ class RequestTopicList(Request):
464
+ command: CommandType = field(default=CommandType.TOPIC_LIST, init=False)
465
+ plugin_id: Optional[str] = None
466
+