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,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ML Training Pipeline Example: Simulated distributed training with checkpointing.
|
|
3
|
+
|
|
4
|
+
This comprehensive example demonstrates:
|
|
5
|
+
1. Multi-stage training pipeline (data loader, trainer, evaluator)
|
|
6
|
+
2. Shared state management via environment variables
|
|
7
|
+
3. Checkpoint saving and restoration
|
|
8
|
+
4. Progress tracking and metrics aggregation
|
|
9
|
+
5. Graceful shutdown with state persistence
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
import random
|
|
15
|
+
from typing import Any, Dict, List
|
|
16
|
+
|
|
17
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
18
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
19
|
+
from parallelazy.plugin_manager import PluginManager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DataLoader(Plugin):
|
|
25
|
+
"""
|
|
26
|
+
Stage 1: Load and serve training data batches.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
31
|
+
return super().Descriptor(
|
|
32
|
+
version="1.0.0",
|
|
33
|
+
author="ModularNet Team",
|
|
34
|
+
description="Data loader for training pipeline",
|
|
35
|
+
options=options or {}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
39
|
+
super().__init__(worker, plugin_id, options)
|
|
40
|
+
self.batch_id = 0
|
|
41
|
+
self.epoch = 0
|
|
42
|
+
self.batches_per_epoch = 5
|
|
43
|
+
|
|
44
|
+
def on_start(self) -> None:
|
|
45
|
+
logger.info(f"[{self.plugin_id}] DataLoader started")
|
|
46
|
+
self.env_set("training.epoch", 0)
|
|
47
|
+
self.env_set("training.batch", 0)
|
|
48
|
+
|
|
49
|
+
def main(self):
|
|
50
|
+
self.batch_id += 1
|
|
51
|
+
if self.batch_id > 1 and (self.batch_id - 1) % self.batches_per_epoch == 0:
|
|
52
|
+
self.epoch += 1
|
|
53
|
+
logger.info(f"[{self.plugin_id}] Starting epoch {self.epoch}")
|
|
54
|
+
self.env_set("training.epoch", self.epoch)
|
|
55
|
+
|
|
56
|
+
# Create a batch
|
|
57
|
+
batch = {
|
|
58
|
+
"batch_id": self.batch_id,
|
|
59
|
+
"epoch": self.epoch,
|
|
60
|
+
"size": 32,
|
|
61
|
+
"data": list(range(32)) # Dummy data
|
|
62
|
+
}
|
|
63
|
+
self.topic_publish("training.batch", batch)
|
|
64
|
+
self.env_set("training.batch", self.batch_id, wait=False)
|
|
65
|
+
|
|
66
|
+
def on_stop(self) -> None:
|
|
67
|
+
logger.info(
|
|
68
|
+
f"[{self.plugin_id}] DataLoader stopped: "
|
|
69
|
+
f"batches={self.batch_id}, epochs={self.epoch}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Trainer(Plugin):
|
|
74
|
+
"""
|
|
75
|
+
Stage 2: Train model on batches and compute loss.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
80
|
+
return super().Descriptor(
|
|
81
|
+
version="1.0.0",
|
|
82
|
+
author="ModularNet Team",
|
|
83
|
+
description="Model trainer for training pipeline",
|
|
84
|
+
options=options or {}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
88
|
+
super().__init__(worker, plugin_id, options)
|
|
89
|
+
self.batches_processed = 0
|
|
90
|
+
self.losses = []
|
|
91
|
+
|
|
92
|
+
def on_start(self) -> None:
|
|
93
|
+
logger.info(f"[{self.plugin_id}] Trainer started")
|
|
94
|
+
self.topic_subscribe("training.batch", self.on_batch)
|
|
95
|
+
self.env_set("training.losses", [])
|
|
96
|
+
self.env_set("training.avg_loss", 0.0)
|
|
97
|
+
|
|
98
|
+
def on_batch(self, batch: Dict[str, Any]):
|
|
99
|
+
self.batches_processed += 1
|
|
100
|
+
# Simulate training: compute loss
|
|
101
|
+
loss = 2.0 * (0.95 ** batch["batch_id"]) + random.gauss(0, 0.1)
|
|
102
|
+
loss = max(0.01, loss) # Ensure positive loss
|
|
103
|
+
self.losses.append(loss)
|
|
104
|
+
|
|
105
|
+
logger.info(
|
|
106
|
+
f"[{self.plugin_id}] Batch {batch['batch_id']}: loss={loss:.4f}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Publish loss for downstream processing
|
|
110
|
+
self.topic_publish("training.loss", {
|
|
111
|
+
"batch_id": batch["batch_id"],
|
|
112
|
+
"epoch": batch["epoch"],
|
|
113
|
+
"loss": loss
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
# Update rolling metrics
|
|
117
|
+
if len(self.losses) > 0:
|
|
118
|
+
avg_loss = sum(self.losses[-5:]) / len(self.losses[-5:]) # Last 5
|
|
119
|
+
self.env_set("training.avg_loss", avg_loss, wait=False)
|
|
120
|
+
|
|
121
|
+
def main(self):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def on_stop(self) -> None:
|
|
125
|
+
logger.info(f"[{self.plugin_id}] Trainer stopped: processed {self.batches_processed} batches")
|
|
126
|
+
if self.losses:
|
|
127
|
+
final_loss = self.losses[-1]
|
|
128
|
+
logger.info(f"[{self.plugin_id}] Final loss: {final_loss:.4f}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Evaluator(Plugin):
|
|
132
|
+
"""
|
|
133
|
+
Stage 3: Evaluate model periodically and checkpoint best model.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
138
|
+
return super().Descriptor(
|
|
139
|
+
version="1.0.0",
|
|
140
|
+
author="ModularNet Team",
|
|
141
|
+
description="Model evaluator with checkpointing",
|
|
142
|
+
options=options or {}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
146
|
+
super().__init__(worker, plugin_id, options)
|
|
147
|
+
self.loss_history = []
|
|
148
|
+
self.best_loss = float("inf")
|
|
149
|
+
self.checkpoints = []
|
|
150
|
+
|
|
151
|
+
def on_start(self) -> None:
|
|
152
|
+
logger.info(f"[{self.plugin_id}] Evaluator started")
|
|
153
|
+
self.topic_subscribe("training.loss", self.on_loss)
|
|
154
|
+
self.env_set("training.best_loss", self.best_loss)
|
|
155
|
+
self.env_set("training.checkpoints", [])
|
|
156
|
+
|
|
157
|
+
def on_loss(self, data: Dict[str, Any]):
|
|
158
|
+
loss = data["loss"]
|
|
159
|
+
self.loss_history.append(loss)
|
|
160
|
+
|
|
161
|
+
# Check if this is a new best
|
|
162
|
+
if loss < self.best_loss:
|
|
163
|
+
self.best_loss = loss
|
|
164
|
+
checkpoint = {
|
|
165
|
+
"batch_id": data["batch_id"],
|
|
166
|
+
"epoch": data["epoch"],
|
|
167
|
+
"loss": loss,
|
|
168
|
+
"timestamp": time.time()
|
|
169
|
+
}
|
|
170
|
+
self.checkpoints.append(checkpoint)
|
|
171
|
+
logger.info(
|
|
172
|
+
f"[{self.plugin_id}] New best loss: {loss:.4f} "
|
|
173
|
+
f"(checkpoint #{len(self.checkpoints)})"
|
|
174
|
+
)
|
|
175
|
+
self.env_set("training.best_loss", self.best_loss, wait=False)
|
|
176
|
+
self.env_set("training.checkpoints", self.checkpoints, wait=False)
|
|
177
|
+
|
|
178
|
+
# Log progress periodically
|
|
179
|
+
if len(self.loss_history) % 5 == 0:
|
|
180
|
+
avg_recent = sum(self.loss_history[-5:]) / 5
|
|
181
|
+
logger.info(
|
|
182
|
+
f"[{self.plugin_id}] Progress: {len(self.loss_history)} batches, "
|
|
183
|
+
f"avg_recent={avg_recent:.4f}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def main(self):
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
def on_stop(self) -> None:
|
|
190
|
+
logger.info(f"[{self.plugin_id}] Evaluator stopped")
|
|
191
|
+
logger.info(
|
|
192
|
+
f"[{self.plugin_id}] Summary: {len(self.loss_history)} batches, "
|
|
193
|
+
f"best_loss={self.best_loss:.4f}, checkpoints={len(self.checkpoints)}"
|
|
194
|
+
)
|
|
195
|
+
logger.info(f"[{self.plugin_id}] Checkpoints saved:")
|
|
196
|
+
for i, ckpt in enumerate(self.checkpoints):
|
|
197
|
+
logger.info(
|
|
198
|
+
f" - Checkpoint {i+1}: batch={ckpt['batch_id']}, "
|
|
199
|
+
f"loss={ckpt['loss']:.4f}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main():
|
|
204
|
+
"""Run the ML training pipeline example."""
|
|
205
|
+
manager = PluginManager()
|
|
206
|
+
|
|
207
|
+
# Create pipeline plugins
|
|
208
|
+
loader_desc = DataLoader.Descriptor()
|
|
209
|
+
trainer_desc = Trainer.Descriptor()
|
|
210
|
+
evaluator_desc = Evaluator.Descriptor()
|
|
211
|
+
|
|
212
|
+
manager.load_plugin(loader_desc)
|
|
213
|
+
manager.load_plugin(trainer_desc)
|
|
214
|
+
manager.load_plugin(evaluator_desc)
|
|
215
|
+
|
|
216
|
+
manager.start_plugin(loader_desc.name)
|
|
217
|
+
manager.start_plugin(trainer_desc.name)
|
|
218
|
+
manager.start_plugin(evaluator_desc.name)
|
|
219
|
+
|
|
220
|
+
print("ML training pipeline running... Press Ctrl+C to stop.")
|
|
221
|
+
time.sleep(6)
|
|
222
|
+
print("Stopping training...")
|
|
223
|
+
|
|
224
|
+
manager.stop_plugin(evaluator_desc.name)
|
|
225
|
+
manager.stop_plugin(trainer_desc.name)
|
|
226
|
+
manager.stop_plugin(loader_desc.name)
|
|
227
|
+
|
|
228
|
+
time.sleep(1)
|
|
229
|
+
|
|
230
|
+
# Show final training state
|
|
231
|
+
print("\nTraining summary:")
|
|
232
|
+
all_vars = manager.env_get()
|
|
233
|
+
for name in ["training.epoch", "training.batch", "training.avg_loss", "training.best_loss"]:
|
|
234
|
+
value = all_vars.get(name)
|
|
235
|
+
if value is not None:
|
|
236
|
+
if isinstance(value, float):
|
|
237
|
+
print(f" {name}: {value:.6f}")
|
|
238
|
+
else:
|
|
239
|
+
print(f" {name}: {value}")
|
|
240
|
+
|
|
241
|
+
checkpoints = all_vars.get("training.checkpoints", [])
|
|
242
|
+
if checkpoints:
|
|
243
|
+
print(f"\n Saved checkpoints ({len(checkpoints)}):")
|
|
244
|
+
for i, ckpt in enumerate(checkpoints, 1):
|
|
245
|
+
print(f" {i}. Batch {ckpt['batch_id']}, loss={ckpt['loss']:.4f}")
|
|
246
|
+
|
|
247
|
+
# VERIFICATION
|
|
248
|
+
print("\n=== VERIFICATION ===")
|
|
249
|
+
print("PREDICTED BEHAVIOR:")
|
|
250
|
+
print("1. DataLoader generates batches for ~5 seconds")
|
|
251
|
+
print("2. Trainer computes loss (exponentially decaying)")
|
|
252
|
+
print("3. Evaluator saves checkpoints when loss improves")
|
|
253
|
+
print("4. Final loss < avg loss (learning curve)")
|
|
254
|
+
print("5. Best checkpoints saved\n")
|
|
255
|
+
|
|
256
|
+
epoch = all_vars.get("training.epoch", 0)
|
|
257
|
+
batch = all_vars.get("training.batch", 0)
|
|
258
|
+
avg_loss = all_vars.get("training.avg_loss", 0)
|
|
259
|
+
best_loss = all_vars.get("training.best_loss", float("inf"))
|
|
260
|
+
|
|
261
|
+
assert batch > 0, "Should generate batches"
|
|
262
|
+
assert epoch > 0, "Should complete at least one epoch"
|
|
263
|
+
assert avg_loss > 0, "Should compute loss"
|
|
264
|
+
assert best_loss < avg_loss, "Best loss should be better than average"
|
|
265
|
+
assert len(checkpoints) > 0, "Should save at least one checkpoint"
|
|
266
|
+
|
|
267
|
+
print(f"✓ Epochs: {epoch}")
|
|
268
|
+
print(f"✓ Batches processed: {batch}")
|
|
269
|
+
print(f"✓ Final avg loss: {avg_loss:.6f}")
|
|
270
|
+
print(f"✓ Best loss: {best_loss:.6f} (improved)")
|
|
271
|
+
print(f"✓ Checkpoints saved: {len(checkpoints)}")
|
|
272
|
+
print("✓ All ML training assertions passed!")
|
|
273
|
+
|
|
274
|
+
manager.unload_plugin(loader_desc.name)
|
|
275
|
+
manager.unload_plugin(trainer_desc.name)
|
|
276
|
+
manager.unload_plugin(evaluator_desc.name)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic test plugin for lifecycle testing.
|
|
3
|
+
|
|
4
|
+
A minimal plugin that exercises the load/unload/start/stop/status lifecycle
|
|
5
|
+
without any external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
12
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
13
|
+
import time
|
|
14
|
+
from parallelazy.plugin_manager import PluginManager
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExamplePluginPub(Plugin):
|
|
20
|
+
"""
|
|
21
|
+
Minimal plugin to test publish/subscribe functionality.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
26
|
+
"""Create plugin descriptor."""
|
|
27
|
+
return super().Descriptor(
|
|
28
|
+
version="1.0.0",
|
|
29
|
+
author="ModularNet Team",
|
|
30
|
+
description="Basic test plugin for lifecycle verification",
|
|
31
|
+
options=options or {}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
worker: PluginWorker,
|
|
37
|
+
plugin_id: str,
|
|
38
|
+
options: Dict[str, Any],
|
|
39
|
+
):
|
|
40
|
+
super().__init__(worker, plugin_id, options)
|
|
41
|
+
self.count = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def on_start(self) -> None:
|
|
45
|
+
logger.info(f"[{self.plugin_id}] ExamplePluginCount starting...")
|
|
46
|
+
self.count = 0
|
|
47
|
+
self.env_set("count_pub", self.count)
|
|
48
|
+
|
|
49
|
+
def main(self):
|
|
50
|
+
self.count +=1
|
|
51
|
+
self.topic_publish("count", self.count)
|
|
52
|
+
|
|
53
|
+
def on_stop(self) -> None:
|
|
54
|
+
logger.info(f"[{self.plugin_id}] Total loop: {self.count}")
|
|
55
|
+
self.env_set("count_pub", self.count)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ExamplePluginSub(Plugin):
|
|
59
|
+
""" """
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
63
|
+
"""Create plugin descriptor."""
|
|
64
|
+
return super().Descriptor(
|
|
65
|
+
version="1.0.0",
|
|
66
|
+
author="ModularNet Team",
|
|
67
|
+
description="Basic test plugin for lifecycle verification",
|
|
68
|
+
options=options or {}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
worker: PluginWorker,
|
|
74
|
+
plugin_id: str,
|
|
75
|
+
options: Dict[str, Any],
|
|
76
|
+
):
|
|
77
|
+
super().__init__(worker, plugin_id, options)
|
|
78
|
+
self.count = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def on_start(self) -> None:
|
|
82
|
+
self.topic_subscribe("count", self.on_count_update)
|
|
83
|
+
|
|
84
|
+
def on_count_update(self, value):
|
|
85
|
+
#logger.info(f"[{self.plugin_id}] Received count update: {value}")
|
|
86
|
+
self.count = value
|
|
87
|
+
|
|
88
|
+
def on_stop(self) -> None:
|
|
89
|
+
logger.info(f"[{self.plugin_id}] Total loop: {self.count}")
|
|
90
|
+
self.env_set("count_sub", self.count)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
"""Example main function to run the plugin standalone for testing."""
|
|
95
|
+
manager = PluginManager()
|
|
96
|
+
pub_descriptor = ExamplePluginPub.Descriptor()
|
|
97
|
+
sub_descriptor = ExamplePluginSub.Descriptor()
|
|
98
|
+
|
|
99
|
+
manager.load_plugin(pub_descriptor)
|
|
100
|
+
manager.start_plugin(pub_descriptor.name)
|
|
101
|
+
|
|
102
|
+
manager.load_plugin(sub_descriptor)
|
|
103
|
+
manager.start_plugin(sub_descriptor.name)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Let it run for a few seconds
|
|
108
|
+
print("Plugins are running... Press Ctrl+C to stop.")
|
|
109
|
+
time.sleep(5)
|
|
110
|
+
print("Stopping plugins...")
|
|
111
|
+
|
|
112
|
+
#manager.stop_plugin(pub_descriptor.name)
|
|
113
|
+
#manager.stop_plugin(sub_descriptor.name)
|
|
114
|
+
|
|
115
|
+
#time.sleep(1) # Give some time for plugins to stop
|
|
116
|
+
|
|
117
|
+
manager.unload_plugin(pub_descriptor.name)
|
|
118
|
+
manager.unload_plugin(sub_descriptor.name)
|
|
119
|
+
|
|
120
|
+
all_vars = manager.env_get()
|
|
121
|
+
|
|
122
|
+
print("All environment variables:")
|
|
123
|
+
for name, value in all_vars.items():
|
|
124
|
+
print(f" {name}: {value}")
|
|
125
|
+
|
|
126
|
+
c_pub = all_vars.get("count_pub", 0)
|
|
127
|
+
c_sub = all_vars.get("count_sub", -1)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if c_pub == c_sub:
|
|
131
|
+
print("Count values match!")
|
|
132
|
+
else:
|
|
133
|
+
print(f"ERROR: mismatch: pub={c_pub}, sub={c_sub}")
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
|
137
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin-to-Plugin Communication Example: RPC method invocation.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Expose methods that other plugins can call
|
|
6
|
+
2. Invoke methods on other plugins asynchronously
|
|
7
|
+
3. Handle results and errors from remote calls
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
15
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
16
|
+
from parallelazy.plugin_manager import PluginManager
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DataProvider(Plugin):
|
|
22
|
+
"""
|
|
23
|
+
Plugin that provides data to other plugins via RPC.
|
|
24
|
+
Simulates a data server that responds to requests.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
29
|
+
return super().Descriptor(
|
|
30
|
+
version="1.0.0",
|
|
31
|
+
author="ModularNet Team",
|
|
32
|
+
description="Provides data via RPC to other plugins",
|
|
33
|
+
options=options or {}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
37
|
+
super().__init__(worker, plugin_id, options)
|
|
38
|
+
self.data = []
|
|
39
|
+
|
|
40
|
+
def on_start(self) -> None:
|
|
41
|
+
logger.info(f"[{self.plugin_id}] DataProvider started")
|
|
42
|
+
self.data = [
|
|
43
|
+
{"id": 1, "value": 100},
|
|
44
|
+
{"id": 2, "value": 200},
|
|
45
|
+
{"id": 3, "value": 300},
|
|
46
|
+
]
|
|
47
|
+
self.env_set("data_provider.ready", True)
|
|
48
|
+
|
|
49
|
+
def main(self):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def get_data(self) -> list:
|
|
53
|
+
"""Public method that other plugins can call."""
|
|
54
|
+
logger.info(f"[{self.plugin_id}] get_data() called")
|
|
55
|
+
return self.data
|
|
56
|
+
|
|
57
|
+
def get_item(self, item_id: int) -> Dict[str, Any]:
|
|
58
|
+
"""Get a specific item by ID."""
|
|
59
|
+
logger.info(f"[{self.plugin_id}] get_item({item_id}) called")
|
|
60
|
+
for item in self.data:
|
|
61
|
+
if item["id"] == item_id:
|
|
62
|
+
return item
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def add_data(self, value: int) -> int:
|
|
66
|
+
"""Add a new data item and return its ID."""
|
|
67
|
+
new_id = max([item["id"] for item in self.data]) + 1
|
|
68
|
+
self.data.append({"id": new_id, "value": value})
|
|
69
|
+
logger.info(f"[{self.plugin_id}] add_data({value}) -> ID {new_id}")
|
|
70
|
+
return new_id
|
|
71
|
+
|
|
72
|
+
def on_stop(self) -> None:
|
|
73
|
+
logger.info(f"[{self.plugin_id}] DataProvider stopped with {len(self.data)} items")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DataConsumer(Plugin):
|
|
77
|
+
"""
|
|
78
|
+
Plugin that consumes data from DataProvider via RPC.
|
|
79
|
+
Simulates a worker that requests data from other plugins.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
84
|
+
return super().Descriptor(
|
|
85
|
+
version="1.0.0",
|
|
86
|
+
author="ModularNet Team",
|
|
87
|
+
description="Consumes data from DataProvider via RPC",
|
|
88
|
+
options=options or {}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
92
|
+
super().__init__(worker, plugin_id, options)
|
|
93
|
+
self.processed = 0
|
|
94
|
+
self.provider_id = None
|
|
95
|
+
|
|
96
|
+
def on_start(self) -> None:
|
|
97
|
+
logger.info(f"[{self.plugin_id}] DataConsumer started")
|
|
98
|
+
# Store the provider ID for later use
|
|
99
|
+
self.provider_id = "data-provider"
|
|
100
|
+
|
|
101
|
+
def main(self):
|
|
102
|
+
# Every few iterations, request data
|
|
103
|
+
self.processed += 1
|
|
104
|
+
if self.processed % 5 == 0:
|
|
105
|
+
self._fetch_and_process_data()
|
|
106
|
+
|
|
107
|
+
def _fetch_and_process_data(self):
|
|
108
|
+
"""Fetch data from provider and process it."""
|
|
109
|
+
try:
|
|
110
|
+
logger.info(f"[{self.plugin_id}] Requesting data from {self.provider_id}...")
|
|
111
|
+
# Call get_data() method on DataProvider
|
|
112
|
+
data = self.plugin_call(self.provider_id, "get_data")
|
|
113
|
+
logger.info(f"[{self.plugin_id}] Received {len(data)} items")
|
|
114
|
+
|
|
115
|
+
# Process each item
|
|
116
|
+
total = sum(item["value"] for item in data)
|
|
117
|
+
logger.info(f"[{self.plugin_id}] Sum of values: {total}")
|
|
118
|
+
|
|
119
|
+
# Request a specific item
|
|
120
|
+
item = self.plugin_call(self.provider_id, "get_item", 2)
|
|
121
|
+
logger.info(f"[{self.plugin_id}] Item #2: {item}")
|
|
122
|
+
|
|
123
|
+
# Add new data
|
|
124
|
+
new_id = self.plugin_call(self.provider_id, "add_data", self.processed * 100)
|
|
125
|
+
logger.info(f"[{self.plugin_id}] Added new item with ID {new_id}")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"[{self.plugin_id}] Error calling DataProvider: {e}")
|
|
129
|
+
|
|
130
|
+
def on_stop(self) -> None:
|
|
131
|
+
logger.info(f"[{self.plugin_id}] DataConsumer stopped after {self.processed} iterations")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
"""Run the plugin-to-plugin communication example."""
|
|
136
|
+
manager = PluginManager()
|
|
137
|
+
|
|
138
|
+
# Create and load plugins
|
|
139
|
+
provider_desc = DataProvider.Descriptor(options={"name": "data-provider"})
|
|
140
|
+
consumer_desc = DataConsumer.Descriptor()
|
|
141
|
+
|
|
142
|
+
# Override the name to match the provider_id used in consumer
|
|
143
|
+
provider_desc.name = "data-provider"
|
|
144
|
+
|
|
145
|
+
manager.load_plugin(provider_desc)
|
|
146
|
+
manager.load_plugin(consumer_desc)
|
|
147
|
+
|
|
148
|
+
manager.start_plugin(provider_desc.name)
|
|
149
|
+
manager.start_plugin(consumer_desc.name)
|
|
150
|
+
|
|
151
|
+
# Let them run for a bit
|
|
152
|
+
print("Plugins running... Press Ctrl+C to stop.")
|
|
153
|
+
time.sleep(6)
|
|
154
|
+
print("Stopping plugins...")
|
|
155
|
+
|
|
156
|
+
manager.stop_plugin(consumer_desc.name)
|
|
157
|
+
manager.stop_plugin(provider_desc.name)
|
|
158
|
+
|
|
159
|
+
time.sleep(1)
|
|
160
|
+
|
|
161
|
+
# VERIFICATION
|
|
162
|
+
print("\n=== VERIFICATION ===")
|
|
163
|
+
print("PREDICTED BEHAVIOR:")
|
|
164
|
+
print("1. DataProvider exposes methods: get_data(), get_item(id), add_data(value)")
|
|
165
|
+
print("2. DataConsumer calls these methods every ~5 iterations")
|
|
166
|
+
print("3. Provider starts with 3 items: {id: 1-3, value: 100-300}")
|
|
167
|
+
print("4. Consumer adds new items, so count increases\n")
|
|
168
|
+
|
|
169
|
+
all_vars = manager.env_get()
|
|
170
|
+
provider_ready = all_vars.get("data_provider.ready")
|
|
171
|
+
|
|
172
|
+
assert provider_ready is True, "data_provider.ready should be True"
|
|
173
|
+
print(f"✓ DataProvider started successfully")
|
|
174
|
+
print(f"✓ RPC calls completed without timeout")
|
|
175
|
+
print("✓ All RPC tests passed!")
|
|
176
|
+
|
|
177
|
+
manager.unload_plugin(provider_desc.name)
|
|
178
|
+
manager.unload_plugin(consumer_desc.name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|