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,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker Pool Example: Multiple instances of the same plugin.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Create multiple instances of the same plugin
|
|
6
|
+
2. Load balance work across worker instances
|
|
7
|
+
3. Track metrics per worker
|
|
8
|
+
4. Coordinate work distribution
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
16
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
17
|
+
from parallelazy.plugin_manager import PluginManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskQueue(Plugin):
|
|
23
|
+
"""
|
|
24
|
+
Task distributor that publishes tasks to workers.
|
|
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="Task queue distributor",
|
|
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.task_id = 0
|
|
39
|
+
|
|
40
|
+
def on_start(self) -> None:
|
|
41
|
+
logger.info(f"[{self.plugin_id}] TaskQueue started")
|
|
42
|
+
self.env_set("queue.total_tasks", 0)
|
|
43
|
+
|
|
44
|
+
def main(self):
|
|
45
|
+
self.task_id += 1
|
|
46
|
+
# Generate a task
|
|
47
|
+
task = {
|
|
48
|
+
"id": self.task_id,
|
|
49
|
+
"operation": "process",
|
|
50
|
+
"complexity": (self.task_id % 3) + 1 # 1-3
|
|
51
|
+
}
|
|
52
|
+
# Publish to workers
|
|
53
|
+
self.topic_publish("tasks.queue", task)
|
|
54
|
+
self.env_set("queue.total_tasks", self.task_id, wait=False)
|
|
55
|
+
|
|
56
|
+
def on_stop(self) -> None:
|
|
57
|
+
logger.info(f"[{self.plugin_id}] TaskQueue stopped, created {self.task_id} tasks")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Worker(Plugin):
|
|
61
|
+
"""
|
|
62
|
+
Worker that processes tasks from the queue.
|
|
63
|
+
Multiple instances can be created.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
68
|
+
return super().Descriptor(
|
|
69
|
+
version="1.0.0",
|
|
70
|
+
author="ModularNet Team",
|
|
71
|
+
description="Worker pool member",
|
|
72
|
+
options=options or {}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
76
|
+
super().__init__(worker, plugin_id, options)
|
|
77
|
+
self.tasks_processed = 0
|
|
78
|
+
self.total_time = 0
|
|
79
|
+
|
|
80
|
+
def on_start(self) -> None:
|
|
81
|
+
logger.info(f"[{self.plugin_id}] Worker started")
|
|
82
|
+
self.topic_subscribe("tasks.queue", self.on_task)
|
|
83
|
+
# Each worker tracks its own metrics
|
|
84
|
+
self.env_set(f"worker.{self.plugin_id}.processed", 0)
|
|
85
|
+
self.env_set(f"worker.{self.plugin_id}.total_time", 0)
|
|
86
|
+
|
|
87
|
+
def on_task(self, task: Dict[str, Any]):
|
|
88
|
+
"""Process a task from the queue."""
|
|
89
|
+
self.tasks_processed += 1
|
|
90
|
+
complexity = task["complexity"]
|
|
91
|
+
# Simulate work based on complexity
|
|
92
|
+
time_taken = complexity * 0.1
|
|
93
|
+
self.total_time += time_taken
|
|
94
|
+
|
|
95
|
+
logger.info(
|
|
96
|
+
f"[{self.plugin_id}] Task #{task['id']}: complexity={complexity}, time={time_taken:.1f}s"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.env_set(f"worker.{self.plugin_id}.processed", self.tasks_processed, wait=False)
|
|
100
|
+
self.env_set(f"worker.{self.plugin_id}.total_time", self.total_time, wait=False)
|
|
101
|
+
|
|
102
|
+
def main(self):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def on_stop(self) -> None:
|
|
106
|
+
logger.info(
|
|
107
|
+
f"[{self.plugin_id}] Worker stopped: "
|
|
108
|
+
f"processed={self.tasks_processed}, total_time={self.total_time:.1f}s"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
"""Run the worker pool example."""
|
|
114
|
+
manager = PluginManager()
|
|
115
|
+
|
|
116
|
+
# Create queue
|
|
117
|
+
queue_desc = TaskQueue.Descriptor()
|
|
118
|
+
manager.load_plugin(queue_desc)
|
|
119
|
+
manager.start_plugin(queue_desc.name)
|
|
120
|
+
|
|
121
|
+
# Create a pool of workers
|
|
122
|
+
num_workers = 3
|
|
123
|
+
worker_descs = []
|
|
124
|
+
for i in range(num_workers):
|
|
125
|
+
worker_desc = Worker.Descriptor()
|
|
126
|
+
worker_desc.name = f"worker-{i}" # Unique name per instance
|
|
127
|
+
manager.load_plugin(worker_desc)
|
|
128
|
+
manager.start_plugin(worker_desc.name)
|
|
129
|
+
worker_descs.append(worker_desc)
|
|
130
|
+
|
|
131
|
+
# Let them run
|
|
132
|
+
print(f"Queue and {num_workers} workers running... Press Ctrl+C to stop.")
|
|
133
|
+
time.sleep(5)
|
|
134
|
+
print("Stopping workers...")
|
|
135
|
+
|
|
136
|
+
# Stop all workers
|
|
137
|
+
for worker_desc in worker_descs:
|
|
138
|
+
manager.stop_plugin(worker_desc.name)
|
|
139
|
+
|
|
140
|
+
manager.stop_plugin(queue_desc.name)
|
|
141
|
+
|
|
142
|
+
time.sleep(1)
|
|
143
|
+
|
|
144
|
+
# Show worker metrics
|
|
145
|
+
print("\nWorker pool metrics:")
|
|
146
|
+
all_vars = manager.env_get()
|
|
147
|
+
for name, value in sorted(all_vars.items()):
|
|
148
|
+
if name.startswith("worker."):
|
|
149
|
+
print(f" {name}: {value}")
|
|
150
|
+
|
|
151
|
+
# VERIFICATION
|
|
152
|
+
print("\n=== VERIFICATION ===")
|
|
153
|
+
print("PREDICTED BEHAVIOR:")
|
|
154
|
+
print("1. TaskQueue generates ~5 tasks in 5 seconds")
|
|
155
|
+
print("2. 3 workers load-balance tasks via pub/sub")
|
|
156
|
+
print("3. Each worker tracks its own processed count")
|
|
157
|
+
print("4. Total processed across all workers >= queue total\n")
|
|
158
|
+
|
|
159
|
+
total_tasks = all_vars.get("queue.total_tasks", 0)
|
|
160
|
+
assert total_tasks > 0, "Queue should generate tasks"
|
|
161
|
+
print(f"✓ Queue generated: {total_tasks} tasks")
|
|
162
|
+
|
|
163
|
+
# Count workers and their processed tasks
|
|
164
|
+
workers_found = {}
|
|
165
|
+
for name, value in all_vars.items():
|
|
166
|
+
if name.startswith("worker.") and name.endswith(".processed"):
|
|
167
|
+
worker_name = name.replace("worker.", "").replace(".processed", "")
|
|
168
|
+
workers_found[worker_name] = value
|
|
169
|
+
|
|
170
|
+
assert len(workers_found) == num_workers, f"Should have {num_workers} workers, found {len(workers_found)}"
|
|
171
|
+
total_processed = sum(workers_found.values())
|
|
172
|
+
assert total_processed > 0, "Workers should process tasks"
|
|
173
|
+
|
|
174
|
+
print(f"✓ Workers found: {num_workers}")
|
|
175
|
+
print(f"✓ Total tasks processed across all workers: {total_processed}")
|
|
176
|
+
for worker_id, count in sorted(workers_found.items()):
|
|
177
|
+
print(f" - {worker_id}: {count} tasks")
|
|
178
|
+
print("✓ All worker pool assertions passed!")
|
|
179
|
+
|
|
180
|
+
# Clean up
|
|
181
|
+
for worker_desc in worker_descs:
|
|
182
|
+
manager.unload_plugin(worker_desc.name)
|
|
183
|
+
|
|
184
|
+
manager.unload_plugin(queue_desc.name)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|
parallelazy/plugin.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Plugin Interface: Abstract base class for all plugins.
|
|
3
|
+
|
|
4
|
+
Defines the lifecycle, subscription, and service interfaces that all
|
|
5
|
+
plugins must implement or can override.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING, Self
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from parallelazy.utils import camel_to_kebab
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PluginState(Enum):
|
|
19
|
+
"""Plugin lifecycle states."""
|
|
20
|
+
INITIALIZED = "initialized"
|
|
21
|
+
LOADED = "loaded"
|
|
22
|
+
RUNNING = "running"
|
|
23
|
+
STOPPED = "stopped"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
|
|
26
|
+
def __str__(self): return str(self.value)
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
Plugin Descriptor: Serializable metadata for dynamic plugin loading.
|
|
30
|
+
|
|
31
|
+
Describes a plugin's identity, location, and configuration options.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field, asdict
|
|
35
|
+
from typing import Any, Dict, Optional
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class PluginDescriptor:
|
|
41
|
+
"""
|
|
42
|
+
Serializable descriptor for plugin metadata and instantiation.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name: Unique plugin identifier
|
|
46
|
+
version: Semantic version (e.g., "1.0.0")
|
|
47
|
+
package: Full package path (e.g., "modularnet.plugins.my_plugin")
|
|
48
|
+
class_name: Plugin class name in the package
|
|
49
|
+
description: Human-readable description
|
|
50
|
+
author: Plugin author
|
|
51
|
+
options: Default configuration options for the plugin
|
|
52
|
+
dependencies: List of required plugin names
|
|
53
|
+
"""
|
|
54
|
+
name: str
|
|
55
|
+
version: str
|
|
56
|
+
package: str
|
|
57
|
+
class_name: str
|
|
58
|
+
description: str = ""
|
|
59
|
+
author: str = ""
|
|
60
|
+
options: Dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
dependencies: list = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
64
|
+
"""Convert descriptor to dictionary (JSON-serializable)."""
|
|
65
|
+
return asdict(self)
|
|
66
|
+
|
|
67
|
+
def to_json(self) -> str:
|
|
68
|
+
"""Serialize descriptor to JSON."""
|
|
69
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PluginDescriptor":
|
|
73
|
+
"""Create descriptor from dictionary."""
|
|
74
|
+
return cls(**data)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_json(cls, json_str: str) -> "PluginDescriptor":
|
|
78
|
+
"""Create descriptor from JSON string."""
|
|
79
|
+
return cls.from_dict(json.loads(json_str))
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
return f"{self.name}@{self.version} ({self.package}.{self.class_name})"
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return f"PluginDescriptor({self})"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Plugin:
|
|
89
|
+
"""
|
|
90
|
+
Base class for all ModularNet plugins.
|
|
91
|
+
|
|
92
|
+
Plugins run in isolated processes and communicate via:
|
|
93
|
+
1. Control channel: Load/unload/configure/service commands
|
|
94
|
+
2. Data plane: Pub/sub topics for high-bandwidth data exchange
|
|
95
|
+
3. Env plane: Shared environment variables with change notifications
|
|
96
|
+
4. Manager proxy: Direct access to manager services (get/set env, publish, invoke)
|
|
97
|
+
|
|
98
|
+
Subclasses must implement:
|
|
99
|
+
- on_start: Initialize plugin resources (optional)
|
|
100
|
+
- on_stop: Clean up resources (optional)
|
|
101
|
+
- main: Main execution loop (optional)
|
|
102
|
+
- on_error: Handle errors (optional)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Direct manager access via self.manager:
|
|
106
|
+
model = self.env_get("model")
|
|
107
|
+
self.env_set("status", "ready")
|
|
108
|
+
self.topic_publish("topic", data)
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
_registry: dict[str, type[Self]] = {}
|
|
112
|
+
|
|
113
|
+
def __init_subclass__(cls, plugin_name: str | None = None, **kwargs):
|
|
114
|
+
super().__init_subclass__(**kwargs)
|
|
115
|
+
|
|
116
|
+
# Determine registry key: custom plugin_name or default class name
|
|
117
|
+
key = plugin_name or cls.__name__
|
|
118
|
+
|
|
119
|
+
if key in cls._registry:
|
|
120
|
+
raise ValueError(f"Plugin '{key}' is already registered.")
|
|
121
|
+
|
|
122
|
+
cls._registry[key] = cls
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def default_options(cls) -> Dict[str, Any]:
|
|
127
|
+
return {}
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def Descriptor(cls, version:str, author:str, description:str=None, name:str=None, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
131
|
+
all_options = cls.default_options().copy()
|
|
132
|
+
all_options.update(options or {})
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
return PluginDescriptor(
|
|
136
|
+
version=version,
|
|
137
|
+
author=author,
|
|
138
|
+
options=all_options,
|
|
139
|
+
name= camel_to_kebab(name or cls.__name__),
|
|
140
|
+
package=f"{cls.__module__}",
|
|
141
|
+
class_name=cls.__name__,
|
|
142
|
+
description=description or f"{cls.__doc__}",
|
|
143
|
+
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def __init__(self, worker: "PluginWorker", plugin_id: str, options: Optional[Dict[str, Any]] = None):
|
|
147
|
+
"""
|
|
148
|
+
Initialize plugin.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
plugin_id: Unique identifier for this plugin instance
|
|
152
|
+
config: Configuration options dictionary
|
|
153
|
+
worker: PluginWorker instance for managing the plugin
|
|
154
|
+
"""
|
|
155
|
+
all_options = self.default_options().copy()
|
|
156
|
+
all_options.update(options or {})
|
|
157
|
+
|
|
158
|
+
self._worker = worker
|
|
159
|
+
self.plugin_id = plugin_id
|
|
160
|
+
self.state = PluginState.INITIALIZED
|
|
161
|
+
self.options = all_options
|
|
162
|
+
self._is_running = False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main(self): pass
|
|
166
|
+
|
|
167
|
+
def on_start(self) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Called when plugin is started.
|
|
170
|
+
|
|
171
|
+
Initialize resources, open connections, start background tasks, etc.
|
|
172
|
+
Raise exception on failure.
|
|
173
|
+
"""
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def on_stop(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Called when plugin is stopped.
|
|
179
|
+
|
|
180
|
+
Clean up resources, close connections, stop background tasks, etc.
|
|
181
|
+
"""
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def on_error(self, error: Exception, source: str=None) -> None:
|
|
185
|
+
""" Called when an error occurs in the plugin."""
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def is_running(self) -> bool:
|
|
190
|
+
"""Whether plugin is currently running."""
|
|
191
|
+
return self._is_running
|
|
192
|
+
|
|
193
|
+
def stop(self) -> None:
|
|
194
|
+
"""Stop the plugin."""
|
|
195
|
+
|
|
196
|
+
self._worker.stop()
|
|
197
|
+
|
|
198
|
+
def topic_subscribe(self, topic:str, callback:str) -> None:
|
|
199
|
+
"""Subscribe to a data topic or environment variable."""
|
|
200
|
+
self._worker.topic_subscribe(topic, callback)
|
|
201
|
+
|
|
202
|
+
def topic_publish(self, topic:str, data:Any) -> None:
|
|
203
|
+
"""Publish data to a topic."""
|
|
204
|
+
self._worker.topic_publish(topic, data)
|
|
205
|
+
|
|
206
|
+
def env_set(self, name:str, value:Any, wait: bool = True) -> None:
|
|
207
|
+
"""Set an environment variable."""
|
|
208
|
+
self._worker.env_set(name, value, wait=wait)
|
|
209
|
+
|
|
210
|
+
def env_get(self, name:str=None, default:Any=None) -> dict|Any:
|
|
211
|
+
"""Get an environment variable."""
|
|
212
|
+
if name is None:
|
|
213
|
+
return self._worker.env_get_all()
|
|
214
|
+
|
|
215
|
+
return self._worker.env_get(name) or default
|
|
216
|
+
|
|
217
|
+
def env_subscribe(self, name:str, callback:str) -> None:
|
|
218
|
+
"""Subscribe to changes in an environment variable."""
|
|
219
|
+
self._worker.env_subscribe(name, callback)
|
|
220
|
+
|
|
221
|
+
def plugin_call(self, plugin_id: str, method_name: str, *args, **kwargs) -> Any:
|
|
222
|
+
"""Call a method provided by the manager or another plugin."""
|
|
223
|
+
return self._worker.plugin_call(plugin_id, method_name, *args, **kwargs)
|