dory-processor-sdk 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.
- dory/__init__.py +101 -0
- dory/auth/__init__.py +10 -0
- dory/auth/oauth2.py +153 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +137 -0
- dory/cli/templates.py +123 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +24 -0
- dory/config/loader.py +430 -0
- dory/config/presets.py +73 -0
- dory/config/schema.py +84 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +434 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +564 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/edge/__init__.py +88 -0
- dory/edge/adaptive.py +644 -0
- dory/edge/detector.py +546 -0
- dory/edge/fencing.py +488 -0
- dory/edge/heartbeat.py +598 -0
- dory/edge/role.py +419 -0
- dory/errors/__init__.py +139 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +498 -0
- dory/geo/__init__.py +40 -0
- dory/geo/geolocalizer.py +1034 -0
- dory/health/__init__.py +12 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +635 -0
- dory/k8s/__init__.py +80 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/labels.py +505 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +148 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +46 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +325 -0
- dory/middleware/request_tracker.py +511 -0
- dory/migration/__init__.py +33 -0
- dory/migration/configmap.py +232 -0
- dory/migration/s3_store.py +594 -0
- dory/migration/serialization.py +135 -0
- dory/migration/state_manager.py +286 -0
- dory/migration/transfer.py +382 -0
- dory/monitoring/__init__.py +29 -0
- dory/monitoring/opentelemetry.py +489 -0
- dory/output/__init__.py +31 -0
- dory/output/envelope.py +137 -0
- dory/output/formatter.py +113 -0
- dory/output/rabbitmq.py +632 -0
- dory/output/routing.py +318 -0
- dory/output/validator.py +199 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +487 -0
- dory/recovery/golden_snapshot.py +713 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +482 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +183 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +457 -0
- dory/resilience/retry.py +389 -0
- dory/simple.py +342 -0
- dory/types.py +68 -0
- dory/utils/__init__.py +31 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_processor_sdk-0.0.1.dist-info/METADATA +424 -0
- dory_processor_sdk-0.0.1.dist-info/RECORD +86 -0
- dory_processor_sdk-0.0.1.dist-info/WHEEL +5 -0
- dory_processor_sdk-0.0.1.dist-info/entry_points.txt +2 -0
- dory_processor_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
- dory_processor_sdk-0.0.1.dist-info/top_level.txt +1 -0
dory/core/lifecycle.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LifecycleManager - Manages processor lifecycle state machine.
|
|
3
|
+
|
|
4
|
+
Handles transitions between lifecycle states and enforces valid
|
|
5
|
+
state transitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from dory.types import LifecycleState
|
|
13
|
+
from dory.utils.errors import DoryStartupError, DoryShutdownError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from dory.core.processor import BaseProcessor
|
|
17
|
+
from dory.core.context import ExecutionContext
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LifecycleManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages the processor lifecycle state machine.
|
|
25
|
+
|
|
26
|
+
States:
|
|
27
|
+
CREATED -> STARTING -> RUNNING -> SHUTTING_DOWN -> STOPPED
|
|
28
|
+
|
|
|
29
|
+
v
|
|
30
|
+
FAILED (from any state on error)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Valid state transitions
|
|
34
|
+
VALID_TRANSITIONS: dict[LifecycleState, set[LifecycleState]] = {
|
|
35
|
+
LifecycleState.CREATED: {LifecycleState.STARTING, LifecycleState.FAILED},
|
|
36
|
+
LifecycleState.STARTING: {LifecycleState.RUNNING, LifecycleState.FAILED},
|
|
37
|
+
LifecycleState.RUNNING: {LifecycleState.SHUTTING_DOWN, LifecycleState.FAILED},
|
|
38
|
+
LifecycleState.SHUTTING_DOWN: {LifecycleState.STOPPED, LifecycleState.FAILED},
|
|
39
|
+
LifecycleState.STOPPED: set(), # Terminal state
|
|
40
|
+
LifecycleState.FAILED: set(), # Terminal state
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._state = LifecycleState.CREATED
|
|
45
|
+
self._state_lock = asyncio.Lock()
|
|
46
|
+
self._state_changed = asyncio.Event()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> LifecycleState:
|
|
50
|
+
"""Current lifecycle state."""
|
|
51
|
+
return self._state
|
|
52
|
+
|
|
53
|
+
def is_running(self) -> bool:
|
|
54
|
+
"""Check if processor is in running state."""
|
|
55
|
+
return self._state == LifecycleState.RUNNING
|
|
56
|
+
|
|
57
|
+
def is_stopped(self) -> bool:
|
|
58
|
+
"""Check if processor has stopped (gracefully or failed)."""
|
|
59
|
+
return self._state in (LifecycleState.STOPPED, LifecycleState.FAILED)
|
|
60
|
+
|
|
61
|
+
def is_shutting_down(self) -> bool:
|
|
62
|
+
"""Check if shutdown is in progress."""
|
|
63
|
+
return self._state == LifecycleState.SHUTTING_DOWN
|
|
64
|
+
|
|
65
|
+
async def transition_to(self, new_state: LifecycleState) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Transition to a new lifecycle state.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
new_state: Target state
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If transition is not valid
|
|
74
|
+
"""
|
|
75
|
+
async with self._state_lock:
|
|
76
|
+
if new_state not in self.VALID_TRANSITIONS.get(self._state, set()):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Invalid state transition: {self._state.name} -> {new_state.name}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
old_state = self._state
|
|
82
|
+
self._state = new_state
|
|
83
|
+
self._state_changed.set()
|
|
84
|
+
self._state_changed.clear()
|
|
85
|
+
|
|
86
|
+
logger.debug(f"Lifecycle transition: {old_state.name} -> {new_state.name}")
|
|
87
|
+
|
|
88
|
+
async def wait_for_state(
|
|
89
|
+
self,
|
|
90
|
+
target_states: set[LifecycleState],
|
|
91
|
+
timeout: float | None = None,
|
|
92
|
+
) -> LifecycleState:
|
|
93
|
+
"""
|
|
94
|
+
Wait for lifecycle to reach one of the target states.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
target_states: Set of states to wait for
|
|
98
|
+
timeout: Maximum time to wait (None = forever)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The state that was reached
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
asyncio.TimeoutError: If timeout exceeded
|
|
105
|
+
"""
|
|
106
|
+
while self._state not in target_states:
|
|
107
|
+
try:
|
|
108
|
+
await asyncio.wait_for(
|
|
109
|
+
self._state_changed.wait(),
|
|
110
|
+
timeout=timeout,
|
|
111
|
+
)
|
|
112
|
+
except asyncio.TimeoutError:
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
return self._state
|
|
116
|
+
|
|
117
|
+
async def run_startup(
|
|
118
|
+
self,
|
|
119
|
+
processor: "BaseProcessor",
|
|
120
|
+
timeout: float = 60.0,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Run processor startup with timeout.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
processor: Processor instance to start
|
|
127
|
+
timeout: Maximum time for startup (seconds)
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
DoryStartupError: If startup fails or times out
|
|
131
|
+
"""
|
|
132
|
+
await self.transition_to(LifecycleState.STARTING)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
await asyncio.wait_for(
|
|
136
|
+
processor.startup(),
|
|
137
|
+
timeout=timeout,
|
|
138
|
+
)
|
|
139
|
+
await self.transition_to(LifecycleState.RUNNING)
|
|
140
|
+
logger.info("Processor startup completed")
|
|
141
|
+
|
|
142
|
+
except asyncio.TimeoutError:
|
|
143
|
+
await self.transition_to(LifecycleState.FAILED)
|
|
144
|
+
raise DoryStartupError(f"Startup timed out after {timeout}s")
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
await self.transition_to(LifecycleState.FAILED)
|
|
148
|
+
raise DoryStartupError(f"Startup failed: {e}", cause=e)
|
|
149
|
+
|
|
150
|
+
async def run_shutdown(
|
|
151
|
+
self,
|
|
152
|
+
processor: "BaseProcessor",
|
|
153
|
+
timeout: float = 30.0,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Run processor shutdown with timeout.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
processor: Processor instance to shutdown
|
|
160
|
+
timeout: Maximum time for shutdown (seconds)
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
DoryShutdownError: If shutdown times out
|
|
164
|
+
"""
|
|
165
|
+
if self._state in (LifecycleState.STOPPED, LifecycleState.FAILED):
|
|
166
|
+
return # Already stopped
|
|
167
|
+
|
|
168
|
+
await self.transition_to(LifecycleState.SHUTTING_DOWN)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
await asyncio.wait_for(
|
|
172
|
+
processor.shutdown(),
|
|
173
|
+
timeout=timeout,
|
|
174
|
+
)
|
|
175
|
+
await self.transition_to(LifecycleState.STOPPED)
|
|
176
|
+
logger.info("Processor shutdown completed")
|
|
177
|
+
|
|
178
|
+
except asyncio.TimeoutError:
|
|
179
|
+
logger.error(f"Shutdown timed out after {timeout}s, forcing exit")
|
|
180
|
+
await self.transition_to(LifecycleState.FAILED)
|
|
181
|
+
raise DoryShutdownError(f"Shutdown timed out after {timeout}s")
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
# Log but continue - shutdown should complete
|
|
185
|
+
logger.error(f"Error during shutdown: {e}")
|
|
186
|
+
await self.transition_to(LifecycleState.STOPPED)
|
|
187
|
+
|
|
188
|
+
async def run_main_loop(
|
|
189
|
+
self,
|
|
190
|
+
processor: "BaseProcessor",
|
|
191
|
+
context: "ExecutionContext",
|
|
192
|
+
) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Run processor main loop until shutdown requested.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
processor: Processor instance to run
|
|
198
|
+
context: Execution context
|
|
199
|
+
"""
|
|
200
|
+
if self._state != LifecycleState.RUNNING:
|
|
201
|
+
raise ValueError(f"Cannot run: state is {self._state.name}, expected RUNNING")
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
await processor.run()
|
|
205
|
+
logger.info("Processor run() completed")
|
|
206
|
+
|
|
207
|
+
except asyncio.CancelledError:
|
|
208
|
+
logger.info("Processor run() cancelled")
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Error in processor run(): {e}")
|
|
213
|
+
await self.transition_to(LifecycleState.FAILED)
|
|
214
|
+
raise
|
dory/core/meta.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metaclass for automatic handler instrumentation.
|
|
3
|
+
|
|
4
|
+
Automatically applies @auto_instrument to all async methods
|
|
5
|
+
starting with "handle_" or "_handle_".
|
|
6
|
+
|
|
7
|
+
No manual decorators needed!
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import logging
|
|
12
|
+
from abc import ABCMeta
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AutoInstrumentMeta(ABCMeta):
|
|
19
|
+
"""
|
|
20
|
+
Metaclass that automatically applies @auto_instrument to handler methods.
|
|
21
|
+
|
|
22
|
+
This eliminates the need for developers to add decorators manually.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
class MyProcessor(BaseProcessor, metaclass=AutoInstrumentMeta):
|
|
26
|
+
async def handle_request(self, request):
|
|
27
|
+
# Automatically instrumented!
|
|
28
|
+
# - Request ID generated
|
|
29
|
+
# - Request tracked
|
|
30
|
+
# - Span created
|
|
31
|
+
# - Errors classified
|
|
32
|
+
return {"status": "ok"}
|
|
33
|
+
|
|
34
|
+
async def handle_webhook(self, webhook):
|
|
35
|
+
# Also automatically instrumented!
|
|
36
|
+
return {"received": True}
|
|
37
|
+
|
|
38
|
+
async def internal_method(self):
|
|
39
|
+
# NOT instrumented (doesn't start with handle_)
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
Auto-instrumented methods:
|
|
43
|
+
- async def handle_*(...): Public handlers
|
|
44
|
+
- async def _handle_*(...): Private handlers
|
|
45
|
+
|
|
46
|
+
Not instrumented:
|
|
47
|
+
- Other methods (don't start with handle_)
|
|
48
|
+
- Sync methods
|
|
49
|
+
- Lifecycle methods (startup, shutdown, run)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# List of methods that should NOT be auto-instrumented
|
|
53
|
+
EXCLUDED_METHODS = {
|
|
54
|
+
"startup",
|
|
55
|
+
"shutdown",
|
|
56
|
+
"run",
|
|
57
|
+
"get_state",
|
|
58
|
+
"restore_state",
|
|
59
|
+
"on_state_restore_failed",
|
|
60
|
+
"on_rapid_restart_detected",
|
|
61
|
+
"on_health_check_failed",
|
|
62
|
+
"reset_caches",
|
|
63
|
+
"run_loop",
|
|
64
|
+
"is_shutting_down",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __new__(mcs, name, bases, namespace):
|
|
68
|
+
"""
|
|
69
|
+
Create new class with auto-instrumented handler methods.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Class name
|
|
73
|
+
bases: Base classes
|
|
74
|
+
namespace: Class namespace (attributes and methods)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
New class with auto-instrumented handlers
|
|
78
|
+
"""
|
|
79
|
+
# Import here to avoid circular dependency
|
|
80
|
+
try:
|
|
81
|
+
from dory.auto_instrument import auto_instrument
|
|
82
|
+
except ImportError:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"auto_instrument decorator not available, skipping auto-instrumentation"
|
|
85
|
+
)
|
|
86
|
+
return super().__new__(mcs, name, bases, namespace)
|
|
87
|
+
|
|
88
|
+
# Count of instrumented methods
|
|
89
|
+
instrumented_count = 0
|
|
90
|
+
|
|
91
|
+
# Auto-instrument handler methods
|
|
92
|
+
for attr_name, attr_value in list(namespace.items()):
|
|
93
|
+
# Check if this is an async method
|
|
94
|
+
if not inspect.iscoroutinefunction(attr_value):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Check if method should be instrumented
|
|
98
|
+
should_instrument = False
|
|
99
|
+
|
|
100
|
+
# Instrument methods starting with handle_ or _handle_
|
|
101
|
+
if attr_name.startswith("handle_") or attr_name.startswith("_handle_"):
|
|
102
|
+
should_instrument = True
|
|
103
|
+
|
|
104
|
+
# Don't instrument excluded methods
|
|
105
|
+
if attr_name in mcs.EXCLUDED_METHODS:
|
|
106
|
+
should_instrument = False
|
|
107
|
+
|
|
108
|
+
# Don't instrument special methods
|
|
109
|
+
if attr_name.startswith("__") and attr_name.endswith("__"):
|
|
110
|
+
should_instrument = False
|
|
111
|
+
|
|
112
|
+
# Apply auto-instrumentation
|
|
113
|
+
if should_instrument:
|
|
114
|
+
namespace[attr_name] = auto_instrument(attr_value)
|
|
115
|
+
instrumented_count += 1
|
|
116
|
+
logger.debug(f"Auto-instrumented method: {name}.{attr_name}")
|
|
117
|
+
|
|
118
|
+
if instrumented_count > 0:
|
|
119
|
+
logger.info(f"Auto-instrumented {instrumented_count} methods in {name}")
|
|
120
|
+
|
|
121
|
+
return super().__new__(mcs, name, bases, namespace)
|