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,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment Variables Example: Demonstrate shared state management.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Set and get environment variables in the manager
|
|
6
|
+
2. Subscribe to changes in environment variables
|
|
7
|
+
3. Use environment variables for cross-plugin communication
|
|
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 ConfigWriter(Plugin):
|
|
22
|
+
"""
|
|
23
|
+
Plugin that periodically updates shared configuration.
|
|
24
|
+
Simulates a configuration server updating settings.
|
|
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="Writes configuration to environment variables",
|
|
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.iteration = 0
|
|
39
|
+
|
|
40
|
+
def on_start(self) -> None:
|
|
41
|
+
logger.info(f"[{self.plugin_id}] ConfigWriter started")
|
|
42
|
+
self.env_set("config.batch_size", 32)
|
|
43
|
+
self.env_set("config.learning_rate", 0.001)
|
|
44
|
+
self.env_set("config.model_type", "resnet50")
|
|
45
|
+
|
|
46
|
+
def main(self):
|
|
47
|
+
self.iteration += 1
|
|
48
|
+
# Periodically update configuration
|
|
49
|
+
if self.iteration % 10 == 0:
|
|
50
|
+
new_lr = 0.001 * (0.9 ** (self.iteration // 10))
|
|
51
|
+
self.env_set("config.learning_rate", new_lr)
|
|
52
|
+
logger.info(f"[{self.plugin_id}] Updated learning rate to {new_lr:.6f}")
|
|
53
|
+
|
|
54
|
+
def on_stop(self) -> None:
|
|
55
|
+
logger.info(f"[{self.plugin_id}] ConfigWriter stopped after {self.iteration} iterations")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConfigReader(Plugin):
|
|
60
|
+
"""
|
|
61
|
+
Plugin that subscribes to configuration changes.
|
|
62
|
+
Simulates a worker that reacts to configuration updates.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
67
|
+
return super().Descriptor(
|
|
68
|
+
version="1.0.0",
|
|
69
|
+
author="ModularNet Team",
|
|
70
|
+
description="Reads and reacts to configuration changes",
|
|
71
|
+
options=options or {}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
75
|
+
super().__init__(worker, plugin_id, options)
|
|
76
|
+
self.current_config = {}
|
|
77
|
+
|
|
78
|
+
def on_start(self) -> None:
|
|
79
|
+
logger.info(f"[{self.plugin_id}] ConfigReader started")
|
|
80
|
+
# Subscribe to all config changes
|
|
81
|
+
self.env_subscribe("config.batch_size", self.on_config_change)
|
|
82
|
+
self.env_subscribe("config.learning_rate", self.on_config_change)
|
|
83
|
+
self.env_subscribe("config.model_type", self.on_config_change)
|
|
84
|
+
|
|
85
|
+
def on_config_change(self, value: Any):
|
|
86
|
+
# The callback receives the new value, not the name
|
|
87
|
+
# (the name is implied by which variable we subscribed to)
|
|
88
|
+
logger.info(f"[{self.plugin_id}] Config changed: {value}")
|
|
89
|
+
|
|
90
|
+
def main(self):
|
|
91
|
+
# Read current config without subscribing
|
|
92
|
+
lr = self.env_get("config.learning_rate", 0.001)
|
|
93
|
+
|
|
94
|
+
def on_stop(self) -> None:
|
|
95
|
+
logger.info(f"[{self.plugin_id}] ConfigReader stopped")
|
|
96
|
+
logger.info(f"[{self.plugin_id}] Final config: {self.current_config}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main():
|
|
100
|
+
"""Run the environment variables example."""
|
|
101
|
+
manager = PluginManager()
|
|
102
|
+
|
|
103
|
+
# Create and load plugins
|
|
104
|
+
writer_desc = ConfigWriter.Descriptor()
|
|
105
|
+
reader_desc = ConfigReader.Descriptor()
|
|
106
|
+
|
|
107
|
+
manager.load_plugin(writer_desc)
|
|
108
|
+
manager.load_plugin(reader_desc)
|
|
109
|
+
|
|
110
|
+
manager.start_plugin(writer_desc.name)
|
|
111
|
+
manager.start_plugin(reader_desc.name)
|
|
112
|
+
|
|
113
|
+
# Let them run for a bit
|
|
114
|
+
print("Plugins running... Press Ctrl+C to stop.")
|
|
115
|
+
time.sleep(8)
|
|
116
|
+
print("Stopping plugins...")
|
|
117
|
+
|
|
118
|
+
manager.stop_plugin(writer_desc.name)
|
|
119
|
+
manager.stop_plugin(reader_desc.name)
|
|
120
|
+
|
|
121
|
+
# Show final state
|
|
122
|
+
time.sleep(1)
|
|
123
|
+
all_vars = manager.env_get()
|
|
124
|
+
print("\nFinal environment state:")
|
|
125
|
+
for name, value in all_vars.items():
|
|
126
|
+
if name.startswith("config."):
|
|
127
|
+
print(f" {name}: {value}")
|
|
128
|
+
|
|
129
|
+
# VERIFICATION
|
|
130
|
+
print("\n=== VERIFICATION ===")
|
|
131
|
+
print("PREDICTED BEHAVIOR:")
|
|
132
|
+
print("1. ConfigWriter sets initial config (batch_size=32, learning_rate=0.001, model_type=resnet50)")
|
|
133
|
+
print("2. ConfigReader subscribes and receives updates")
|
|
134
|
+
print("3. Learning rate decays over iterations")
|
|
135
|
+
print("4. Both plugins update same environment variables\n")
|
|
136
|
+
|
|
137
|
+
batch_size = all_vars.get("config.batch_size")
|
|
138
|
+
learning_rate = all_vars.get("config.learning_rate")
|
|
139
|
+
model_type = all_vars.get("config.model_type")
|
|
140
|
+
|
|
141
|
+
assert batch_size is not None, "config.batch_size should be set"
|
|
142
|
+
assert learning_rate is not None, "config.learning_rate should be set"
|
|
143
|
+
assert model_type is not None, "config.model_type should be set"
|
|
144
|
+
assert batch_size == 32, f"batch_size should be 32, got {batch_size}"
|
|
145
|
+
assert model_type == "resnet50", f"model_type should be resnet50, got {model_type}"
|
|
146
|
+
assert learning_rate < 0.001, f"learning_rate should decay below 0.001, got {learning_rate}"
|
|
147
|
+
|
|
148
|
+
print(f"✓ config.batch_size: {batch_size} (unchanged)")
|
|
149
|
+
print(f"✓ config.learning_rate: {learning_rate} (decayed from 0.001)")
|
|
150
|
+
print(f"✓ config.model_type: {model_type} (unchanged)")
|
|
151
|
+
print("✓ All assertions passed!")
|
|
152
|
+
|
|
153
|
+
manager.unload_plugin(writer_desc.name)
|
|
154
|
+
manager.unload_plugin(reader_desc.name)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Handling Example: Resilient plugins with error recovery.
|
|
3
|
+
|
|
4
|
+
This example shows how plugins can:
|
|
5
|
+
1. Handle errors gracefully
|
|
6
|
+
2. Retry operations
|
|
7
|
+
3. Report errors to manager
|
|
8
|
+
4. Implement fallback strategies
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
import random
|
|
14
|
+
from typing import Any, Dict
|
|
15
|
+
|
|
16
|
+
from parallelazy.plugin import Plugin, PluginDescriptor
|
|
17
|
+
from parallelazy.plugin_worker import PluginWorker
|
|
18
|
+
from parallelazy.plugin_manager import PluginManager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UnreliableService(Plugin):
|
|
24
|
+
"""
|
|
25
|
+
Plugin that simulates an unreliable external service.
|
|
26
|
+
Randomly fails to simulate real-world conditions.
|
|
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="Unreliable service simulator",
|
|
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.success_count = 0
|
|
41
|
+
self.failure_count = 0
|
|
42
|
+
self.failure_rate = 0.3 # 30% failure rate
|
|
43
|
+
|
|
44
|
+
def on_start(self) -> None:
|
|
45
|
+
logger.info(f"[{self.plugin_id}] UnreliableService started")
|
|
46
|
+
self.env_set("service.status", "ready")
|
|
47
|
+
self.env_set("service.success_count", 0)
|
|
48
|
+
self.env_set("service.failure_count", 0)
|
|
49
|
+
|
|
50
|
+
def main(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def call_service(self, request_id: int) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Simulates a service call that may fail.
|
|
56
|
+
"""
|
|
57
|
+
if random.random() < self.failure_rate:
|
|
58
|
+
self.failure_count += 1
|
|
59
|
+
logger.error(f"[{self.plugin_id}] Service call #{request_id} FAILED")
|
|
60
|
+
self.env_set("service.failure_count", self.failure_count, wait=False)
|
|
61
|
+
raise Exception(f"Service temporarily unavailable (request #{request_id})")
|
|
62
|
+
else:
|
|
63
|
+
self.success_count += 1
|
|
64
|
+
result = {"request_id": request_id, "data": f"response_{request_id}"}
|
|
65
|
+
logger.info(f"[{self.plugin_id}] Service call #{request_id} SUCCESS")
|
|
66
|
+
self.env_set("service.success_count", self.success_count, wait=False)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def on_stop(self) -> None:
|
|
70
|
+
logger.info(
|
|
71
|
+
f"[{self.plugin_id}] UnreliableService stopped: "
|
|
72
|
+
f"success={self.success_count}, failures={self.failure_count}"
|
|
73
|
+
)
|
|
74
|
+
self.env_set("service.final_status", "stopped")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ResilientClient(Plugin):
|
|
78
|
+
"""
|
|
79
|
+
Plugin that calls unreliable service with retry logic.
|
|
80
|
+
Demonstrates error handling and recovery strategies.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
85
|
+
return super().Descriptor(
|
|
86
|
+
version="1.0.0",
|
|
87
|
+
author="ModularNet Team",
|
|
88
|
+
description="Resilient service client with retry logic",
|
|
89
|
+
options=options or {}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
93
|
+
super().__init__(worker, plugin_id, options)
|
|
94
|
+
self.requests_sent = 0
|
|
95
|
+
self.requests_succeeded = 0
|
|
96
|
+
self.requests_failed = 0
|
|
97
|
+
self.service_id = "unreliable-service"
|
|
98
|
+
|
|
99
|
+
def on_start(self) -> None:
|
|
100
|
+
logger.info(f"[{self.plugin_id}] ResilientClient started")
|
|
101
|
+
self.env_set("client.requests_sent", 0)
|
|
102
|
+
self.env_set("client.requests_succeeded", 0)
|
|
103
|
+
self.env_set("client.requests_failed", 0)
|
|
104
|
+
|
|
105
|
+
def main(self):
|
|
106
|
+
self.requests_sent += 1
|
|
107
|
+
if self.requests_sent % 3 == 0:
|
|
108
|
+
self._call_with_retry(self.requests_sent)
|
|
109
|
+
|
|
110
|
+
def _call_with_retry(self, request_id: int, max_retries: int = 3):
|
|
111
|
+
"""Call service with retry logic."""
|
|
112
|
+
retry_count = 0
|
|
113
|
+
last_error = None
|
|
114
|
+
|
|
115
|
+
while retry_count < max_retries:
|
|
116
|
+
try:
|
|
117
|
+
logger.info(
|
|
118
|
+
f"[{self.plugin_id}] Calling service (request #{request_id}, "
|
|
119
|
+
f"attempt {retry_count + 1}/{max_retries})"
|
|
120
|
+
)
|
|
121
|
+
result = self.plugin_call(self.service_id, "call_service", request_id)
|
|
122
|
+
logger.info(f"[{self.plugin_id}] Request #{request_id} succeeded: {result}")
|
|
123
|
+
self.requests_succeeded += 1
|
|
124
|
+
self.env_set("client.requests_succeeded", self.requests_succeeded, wait=False)
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
last_error = e
|
|
129
|
+
retry_count += 1
|
|
130
|
+
logger.warning(
|
|
131
|
+
f"[{self.plugin_id}] Request #{request_id} failed (attempt {retry_count}/{max_retries}): {e}"
|
|
132
|
+
)
|
|
133
|
+
if retry_count < max_retries:
|
|
134
|
+
# Brief backoff before retry
|
|
135
|
+
time.sleep(0.1 * retry_count)
|
|
136
|
+
|
|
137
|
+
# All retries exhausted
|
|
138
|
+
logger.error(f"[{self.plugin_id}] Request #{request_id} FAILED after {max_retries} attempts: {last_error}")
|
|
139
|
+
self.requests_failed += 1
|
|
140
|
+
self.env_set("client.requests_failed", self.requests_failed, wait=False)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def on_stop(self) -> None:
|
|
144
|
+
logger.info(
|
|
145
|
+
f"[{self.plugin_id}] ResilientClient stopped: "
|
|
146
|
+
f"sent={self.requests_sent}, succeeded={self.requests_succeeded}, "
|
|
147
|
+
f"failed={self.requests_failed}"
|
|
148
|
+
)
|
|
149
|
+
self.env_set("client.requests_sent", self.requests_sent)
|
|
150
|
+
self.env_set("client.requests_succeeded", self.requests_succeeded)
|
|
151
|
+
self.env_set("client.requests_failed", self.requests_failed)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class HealthMonitor(Plugin):
|
|
155
|
+
"""
|
|
156
|
+
Plugin that monitors the health of other plugins.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
161
|
+
return super().Descriptor(
|
|
162
|
+
version="1.0.0",
|
|
163
|
+
author="ModularNet Team",
|
|
164
|
+
description="Health monitor for system resilience",
|
|
165
|
+
options=options or {}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
169
|
+
super().__init__(worker, plugin_id, options)
|
|
170
|
+
self.checks = 0
|
|
171
|
+
|
|
172
|
+
def on_start(self) -> None:
|
|
173
|
+
logger.info(f"[{self.plugin_id}] HealthMonitor started")
|
|
174
|
+
# Subscribe to service status
|
|
175
|
+
self.env_subscribe("service.status", self.on_service_status_change)
|
|
176
|
+
self.env_set("monitor.checks", 0)
|
|
177
|
+
|
|
178
|
+
def on_service_status_change(self, name: str, value: Any):
|
|
179
|
+
logger.info(f"[{self.plugin_id}] Service status changed: {value}")
|
|
180
|
+
|
|
181
|
+
def main(self):
|
|
182
|
+
self.checks += 1
|
|
183
|
+
if self.checks % 4 == 0:
|
|
184
|
+
# Check service metrics
|
|
185
|
+
success = self.env_get("service.success_count", 0)
|
|
186
|
+
failures = self.env_get("service.failure_count", 0)
|
|
187
|
+
total = success + failures
|
|
188
|
+
if total > 0:
|
|
189
|
+
failure_rate = failures / total
|
|
190
|
+
status = "UNHEALTHY" if failure_rate > 0.5 else "HEALTHY"
|
|
191
|
+
logger.info(
|
|
192
|
+
f"[{self.plugin_id}] Health check: {status} (success_rate={1-failure_rate:.1%})"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def on_stop(self) -> None:
|
|
196
|
+
logger.info(f"[{self.plugin_id}] HealthMonitor stopped after {self.checks} checks")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
"""Run the error handling example."""
|
|
201
|
+
manager = PluginManager()
|
|
202
|
+
|
|
203
|
+
# Create plugins
|
|
204
|
+
service_desc = UnreliableService.Descriptor()
|
|
205
|
+
service_desc.name = "unreliable-service"
|
|
206
|
+
|
|
207
|
+
client_desc = ResilientClient.Descriptor()
|
|
208
|
+
monitor_desc = HealthMonitor.Descriptor()
|
|
209
|
+
|
|
210
|
+
manager.load_plugin(service_desc)
|
|
211
|
+
manager.load_plugin(client_desc)
|
|
212
|
+
manager.load_plugin(monitor_desc)
|
|
213
|
+
|
|
214
|
+
manager.start_plugin(service_desc.name)
|
|
215
|
+
manager.start_plugin(client_desc.name)
|
|
216
|
+
manager.start_plugin(monitor_desc.name)
|
|
217
|
+
|
|
218
|
+
print("Error handling example running... Press Ctrl+C to stop.")
|
|
219
|
+
time.sleep(6)
|
|
220
|
+
print("Stopping plugins...")
|
|
221
|
+
|
|
222
|
+
manager.stop_plugin(monitor_desc.name)
|
|
223
|
+
manager.stop_plugin(client_desc.name)
|
|
224
|
+
manager.stop_plugin(service_desc.name)
|
|
225
|
+
|
|
226
|
+
time.sleep(1)
|
|
227
|
+
|
|
228
|
+
# Show final metrics
|
|
229
|
+
print("\nFinal metrics:")
|
|
230
|
+
all_vars = manager.env_get()
|
|
231
|
+
for name, value in sorted(all_vars.items()):
|
|
232
|
+
if "service" in name or "client" in name:
|
|
233
|
+
print(f" {name}: {value}")
|
|
234
|
+
|
|
235
|
+
# VERIFICATION
|
|
236
|
+
print("\n=== VERIFICATION ===")
|
|
237
|
+
print("PREDICTED BEHAVIOR:")
|
|
238
|
+
print("1. UnreliableService fails ~30% of the time")
|
|
239
|
+
print("2. ResilientClient retries up to 3 times (exponential backoff)")
|
|
240
|
+
print("3. Client succeeds on some retries, fails on others")
|
|
241
|
+
print("4. Both success and failure metrics tracked\n")
|
|
242
|
+
|
|
243
|
+
service_success = all_vars.get("service.success_count", 0)
|
|
244
|
+
service_failures = all_vars.get("service.failure_count", 0)
|
|
245
|
+
client_success = all_vars.get("client.requests_succeeded", 0)
|
|
246
|
+
client_failures = all_vars.get("client.requests_failed", 0)
|
|
247
|
+
|
|
248
|
+
assert service_success > 0, "Service should have successes"
|
|
249
|
+
assert service_failures > 0, "Service should have failures (30% rate)"
|
|
250
|
+
assert client_success > 0, "Client should retry and succeed sometimes"
|
|
251
|
+
|
|
252
|
+
service_total = service_success + service_failures
|
|
253
|
+
failure_rate = service_failures / service_total if service_total > 0 else 0
|
|
254
|
+
|
|
255
|
+
print(f"✓ Service stats: success={service_success}, failures={service_failures}, failure_rate={failure_rate:.1%}")
|
|
256
|
+
print(f"✓ Client resilience: succeeded={client_success}, failed={client_failures}")
|
|
257
|
+
print(f"✓ Retry strategy working (some requests succeeded despite service failures)")
|
|
258
|
+
print("✓ All error handling assertions passed!")
|
|
259
|
+
|
|
260
|
+
manager.unload_plugin(service_desc.name)
|
|
261
|
+
manager.unload_plugin(client_desc.name)
|
|
262
|
+
manager.unload_plugin(monitor_desc.name)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graceful Shutdown Example: Verify clean plugin termination.
|
|
3
|
+
|
|
4
|
+
This minimal example demonstrates:
|
|
5
|
+
1. Plugin lifecycle (load, start, stop, unload)
|
|
6
|
+
2. Graceful shutdown with on_stop() callback
|
|
7
|
+
3. Process cleanup and resource release
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
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
|
+
# Configure logging for clear shutdown verification
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.DEBUG,
|
|
22
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
23
|
+
handlers=[logging.StreamHandler(sys.stdout)]
|
|
24
|
+
)
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SimpleWorker(Plugin):
|
|
29
|
+
"""
|
|
30
|
+
Minimal plugin that logs lifecycle events.
|
|
31
|
+
Perfect for verifying graceful shutdown behavior.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def Descriptor(cls, options: Dict[str, Any] = None) -> PluginDescriptor:
|
|
36
|
+
return super().Descriptor(
|
|
37
|
+
version="1.0.0",
|
|
38
|
+
author="ModularNet Team",
|
|
39
|
+
description="Simple worker for testing graceful shutdown",
|
|
40
|
+
options=options or {}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __init__(self, worker: PluginWorker, plugin_id: str, options: Dict[str, Any]):
|
|
44
|
+
super().__init__(worker, plugin_id, options)
|
|
45
|
+
self.iteration = 0
|
|
46
|
+
|
|
47
|
+
def on_start(self) -> None:
|
|
48
|
+
logger.info(f"[{self.plugin_id}] ✓ on_start() called - plugin initialized")
|
|
49
|
+
self.iteration = 0
|
|
50
|
+
|
|
51
|
+
def main(self):
|
|
52
|
+
"""Main loop - runs repeatedly until on_stop() is called."""
|
|
53
|
+
self.iteration += 1
|
|
54
|
+
logger.debug(f"[{self.plugin_id}] Iteration {self.iteration}")
|
|
55
|
+
time.sleep(0.1) # Brief sleep to simulate work
|
|
56
|
+
|
|
57
|
+
def on_stop(self) -> None:
|
|
58
|
+
"""Graceful shutdown callback - called before plugin terminates."""
|
|
59
|
+
logger.info(f"[{self.plugin_id}] ✓ on_stop() called - cleaning up after {self.iteration} iterations")
|
|
60
|
+
logger.info(f"[{self.plugin_id}] ✓ Graceful shutdown complete")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
"""Run the graceful shutdown verification."""
|
|
65
|
+
print("\n" + "=" * 70)
|
|
66
|
+
print("GRACEFUL SHUTDOWN VERIFICATION")
|
|
67
|
+
print("=" * 70 + "\n")
|
|
68
|
+
|
|
69
|
+
# Step 1: Create manager
|
|
70
|
+
print("[STEP 1] Creating PluginManager...")
|
|
71
|
+
manager = PluginManager(timeout_start=5.0)
|
|
72
|
+
print("✓ Manager created\n")
|
|
73
|
+
|
|
74
|
+
# Step 2: Load plugin
|
|
75
|
+
print("[STEP 2] Loading plugin...")
|
|
76
|
+
descriptor = SimpleWorker.Descriptor()
|
|
77
|
+
if not manager.load_plugin(descriptor):
|
|
78
|
+
logger.error("Failed to load plugin")
|
|
79
|
+
return False
|
|
80
|
+
print("✓ Plugin loaded\n")
|
|
81
|
+
|
|
82
|
+
# Step 3: Start plugin
|
|
83
|
+
print("[STEP 3] Starting plugin...")
|
|
84
|
+
if not manager.start_plugin(descriptor.name):
|
|
85
|
+
logger.error("Failed to start plugin")
|
|
86
|
+
return False
|
|
87
|
+
print("✓ Plugin started\n")
|
|
88
|
+
|
|
89
|
+
# Step 4: Let it run briefly
|
|
90
|
+
print("[STEP 4] Running for 2 seconds...")
|
|
91
|
+
time.sleep(2)
|
|
92
|
+
print("✓ Plugin has been running\n")
|
|
93
|
+
|
|
94
|
+
# Step 5: Stop plugin (graceful shutdown)
|
|
95
|
+
print("[STEP 5] Stopping plugin (graceful shutdown)...")
|
|
96
|
+
if not manager.stop_plugin(descriptor.name):
|
|
97
|
+
logger.error("Failed to stop plugin")
|
|
98
|
+
return False
|
|
99
|
+
print("✓ Plugin stopped gracefully\n")
|
|
100
|
+
|
|
101
|
+
# Step 6: Unload plugin
|
|
102
|
+
print("[STEP 6] Unloading plugin...")
|
|
103
|
+
if not manager.unload_plugin(descriptor.name):
|
|
104
|
+
logger.error("Failed to unload plugin")
|
|
105
|
+
return False
|
|
106
|
+
print("✓ Plugin unloaded\n")
|
|
107
|
+
|
|
108
|
+
# Step 7: Verify shutdown
|
|
109
|
+
print("[STEP 7] Verification...")
|
|
110
|
+
if descriptor.name in manager._plugins:
|
|
111
|
+
logger.error("Plugin still in manager after unload!")
|
|
112
|
+
return False
|
|
113
|
+
print("✓ Plugin successfully cleaned up from manager\n")
|
|
114
|
+
|
|
115
|
+
# Summary
|
|
116
|
+
print("=" * 70)
|
|
117
|
+
print("GRACEFUL SHUTDOWN VERIFICATION PASSED ✓")
|
|
118
|
+
print("=" * 70)
|
|
119
|
+
print("\nExpected behavior verified:")
|
|
120
|
+
print(" 1. on_start() called when plugin started")
|
|
121
|
+
print(" 2. Plugin ran for ~2 seconds (main loop)")
|
|
122
|
+
print(" 3. on_stop() called when stop_plugin() invoked")
|
|
123
|
+
print(" 4. Process terminated cleanly")
|
|
124
|
+
print(" 5. Resources cleaned up after unload_plugin()")
|
|
125
|
+
print("\n")
|
|
126
|
+
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
try:
|
|
132
|
+
success = main()
|
|
133
|
+
sys.exit(0 if success else 1)
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
print("\nInterrupted by user")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Unexpected error: {e}", exc_info=True)
|
|
139
|
+
sys.exit(1)
|