dory-sdk 2.1.0__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 +70 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +290 -0
- dory/cli/templates.py +333 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +50 -0
- dory/config/loader.py +361 -0
- dory/config/presets.py +325 -0
- dory/config/schema.py +152 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +404 -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 +654 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/errors/__init__.py +117 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +495 -0
- dory/health/__init__.py +10 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +306 -0
- dory/k8s/__init__.py +11 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +175 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +36 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +321 -0
- dory/middleware/request_tracker.py +501 -0
- dory/migration/__init__.py +11 -0
- dory/migration/configmap.py +260 -0
- dory/migration/serialization.py +167 -0
- dory/migration/state_manager.py +301 -0
- dory/monitoring/__init__.py +23 -0
- dory/monitoring/opentelemetry.py +462 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +480 -0
- dory/recovery/golden_snapshot.py +561 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +479 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +187 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +454 -0
- dory/resilience/retry.py +389 -0
- dory/sidecar/__init__.py +6 -0
- dory/sidecar/main.py +75 -0
- dory/sidecar/server.py +329 -0
- dory/simple.py +342 -0
- dory/types.py +75 -0
- dory/utils/__init__.py +25 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_sdk-2.1.0.dist-info/METADATA +663 -0
- dory_sdk-2.1.0.dist-info/RECORD +69 -0
- dory_sdk-2.1.0.dist-info/WHEEL +5 -0
- dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
- dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
dory/core/signals.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SignalHandler - Handles OS signals for graceful shutdown.
|
|
3
|
+
|
|
4
|
+
Captures SIGTERM, SIGINT, and SIGUSR1 and triggers appropriate
|
|
5
|
+
actions in the SDK.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import signal
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Callable, Awaitable
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SignalHandler:
|
|
18
|
+
"""
|
|
19
|
+
Handles OS signals for graceful shutdown.
|
|
20
|
+
|
|
21
|
+
Signals handled:
|
|
22
|
+
SIGTERM: Graceful shutdown (from Kubelet)
|
|
23
|
+
SIGINT: Graceful shutdown (Ctrl+C for local testing)
|
|
24
|
+
SIGUSR1: Trigger state snapshot (for debugging)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._shutdown_callback: Callable[[], Awaitable[None]] | None = None
|
|
29
|
+
self._snapshot_callback: Callable[[], Awaitable[None]] | None = None
|
|
30
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
31
|
+
self._shutdown_triggered = False
|
|
32
|
+
|
|
33
|
+
def setup(
|
|
34
|
+
self,
|
|
35
|
+
shutdown_callback: Callable[[], Awaitable[None]],
|
|
36
|
+
snapshot_callback: Callable[[], Awaitable[None]] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Setup signal handlers.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
shutdown_callback: Async callback for graceful shutdown
|
|
43
|
+
snapshot_callback: Optional async callback for state snapshot
|
|
44
|
+
"""
|
|
45
|
+
self._shutdown_callback = shutdown_callback
|
|
46
|
+
self._snapshot_callback = snapshot_callback
|
|
47
|
+
self._loop = asyncio.get_event_loop()
|
|
48
|
+
|
|
49
|
+
# Register signal handlers
|
|
50
|
+
if sys.platform != "win32":
|
|
51
|
+
# Unix signals
|
|
52
|
+
self._loop.add_signal_handler(
|
|
53
|
+
signal.SIGTERM,
|
|
54
|
+
self._handle_shutdown_signal,
|
|
55
|
+
"SIGTERM",
|
|
56
|
+
)
|
|
57
|
+
self._loop.add_signal_handler(
|
|
58
|
+
signal.SIGINT,
|
|
59
|
+
self._handle_shutdown_signal,
|
|
60
|
+
"SIGINT",
|
|
61
|
+
)
|
|
62
|
+
self._loop.add_signal_handler(
|
|
63
|
+
signal.SIGUSR1,
|
|
64
|
+
self._handle_snapshot_signal,
|
|
65
|
+
)
|
|
66
|
+
logger.debug("Signal handlers registered (Unix)")
|
|
67
|
+
else:
|
|
68
|
+
# Windows - limited signal support
|
|
69
|
+
signal.signal(signal.SIGTERM, self._handle_shutdown_signal_sync)
|
|
70
|
+
signal.signal(signal.SIGINT, self._handle_shutdown_signal_sync)
|
|
71
|
+
logger.debug("Signal handlers registered (Windows)")
|
|
72
|
+
|
|
73
|
+
def _handle_shutdown_signal(self, sig_name: str) -> None:
|
|
74
|
+
"""Handle SIGTERM/SIGINT asynchronously."""
|
|
75
|
+
if self._shutdown_triggered:
|
|
76
|
+
logger.warning(f"Received {sig_name} but shutdown already in progress")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
self._shutdown_triggered = True
|
|
80
|
+
logger.info(f"Received {sig_name}, initiating graceful shutdown")
|
|
81
|
+
|
|
82
|
+
if self._shutdown_callback and self._loop:
|
|
83
|
+
asyncio.ensure_future(
|
|
84
|
+
self._shutdown_callback(),
|
|
85
|
+
loop=self._loop,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _handle_shutdown_signal_sync(self, signum: int, frame) -> None:
|
|
89
|
+
"""Handle signal synchronously (Windows compatibility)."""
|
|
90
|
+
sig_name = signal.Signals(signum).name
|
|
91
|
+
self._handle_shutdown_signal(sig_name)
|
|
92
|
+
|
|
93
|
+
def _handle_snapshot_signal(self) -> None:
|
|
94
|
+
"""Handle SIGUSR1 for debug state snapshot."""
|
|
95
|
+
logger.info("Received SIGUSR1, triggering state snapshot")
|
|
96
|
+
|
|
97
|
+
if self._snapshot_callback and self._loop:
|
|
98
|
+
asyncio.ensure_future(
|
|
99
|
+
self._snapshot_callback(),
|
|
100
|
+
loop=self._loop,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def remove_handlers(self) -> None:
|
|
104
|
+
"""Remove signal handlers during shutdown."""
|
|
105
|
+
if self._loop and sys.platform != "win32":
|
|
106
|
+
try:
|
|
107
|
+
self._loop.remove_signal_handler(signal.SIGTERM)
|
|
108
|
+
self._loop.remove_signal_handler(signal.SIGINT)
|
|
109
|
+
self._loop.remove_signal_handler(signal.SIGUSR1)
|
|
110
|
+
logger.debug("Signal handlers removed")
|
|
111
|
+
except (ValueError, RuntimeError):
|
|
112
|
+
# Handler not registered or loop closed
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def shutdown_triggered(self) -> bool:
|
|
117
|
+
"""Check if shutdown has been triggered."""
|
|
118
|
+
return self._shutdown_triggered
|
|
119
|
+
|
|
120
|
+
def reset(self) -> None:
|
|
121
|
+
"""Reset shutdown state (for testing)."""
|
|
122
|
+
self._shutdown_triggered = False
|
dory/decorators.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for simplified Dory SDK integration.
|
|
3
|
+
|
|
4
|
+
Provides @stateful decorator for automatic state management,
|
|
5
|
+
eliminating the need to manually implement get_state() and restore_state().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, TypeVar, Generic, Callable
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class StatefulVar(Generic[T]):
|
|
16
|
+
"""
|
|
17
|
+
A stateful variable that automatically participates in state save/restore.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from dory import stateful
|
|
21
|
+
|
|
22
|
+
class MyProcessor(BaseProcessor):
|
|
23
|
+
counter = stateful(0)
|
|
24
|
+
sessions = stateful(dict)
|
|
25
|
+
|
|
26
|
+
async def run(self):
|
|
27
|
+
self.counter += 1 # Automatically saved/restored
|
|
28
|
+
"""
|
|
29
|
+
_default: T | Callable[[], T]
|
|
30
|
+
_name: str = ""
|
|
31
|
+
_value: T = field(default=None, repr=False)
|
|
32
|
+
_initialized: bool = field(default=False, repr=False)
|
|
33
|
+
|
|
34
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
35
|
+
"""Called when descriptor is assigned to a class attribute."""
|
|
36
|
+
self._name = name
|
|
37
|
+
# Register this stateful var with the class
|
|
38
|
+
if not hasattr(owner, '_stateful_vars'):
|
|
39
|
+
owner._stateful_vars = {}
|
|
40
|
+
owner._stateful_vars[name] = self
|
|
41
|
+
|
|
42
|
+
def __get__(self, obj: Any, objtype: type | None = None) -> T:
|
|
43
|
+
"""Get the current value."""
|
|
44
|
+
if obj is None:
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
# Initialize on first access
|
|
48
|
+
if not hasattr(obj, f'_stateful_{self._name}_value'):
|
|
49
|
+
default = self._default() if callable(self._default) else self._default
|
|
50
|
+
setattr(obj, f'_stateful_{self._name}_value', default)
|
|
51
|
+
|
|
52
|
+
return getattr(obj, f'_stateful_{self._name}_value')
|
|
53
|
+
|
|
54
|
+
def __set__(self, obj: Any, value: T) -> None:
|
|
55
|
+
"""Set the value."""
|
|
56
|
+
setattr(obj, f'_stateful_{self._name}_value', value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def stateful(default: T | Callable[[], T] = None) -> StatefulVar[T]:
|
|
60
|
+
"""
|
|
61
|
+
Mark a class attribute as stateful for automatic state management.
|
|
62
|
+
|
|
63
|
+
The SDK will automatically include this attribute in get_state() output
|
|
64
|
+
and restore it in restore_state().
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
default: Default value or factory function (use factory for mutable defaults)
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
class MyProcessor(BaseProcessor):
|
|
71
|
+
# Simple values
|
|
72
|
+
counter = stateful(0)
|
|
73
|
+
name = stateful("default")
|
|
74
|
+
|
|
75
|
+
# Mutable defaults (use factory to avoid sharing)
|
|
76
|
+
sessions = stateful(dict) # Same as stateful(lambda: {})
|
|
77
|
+
items = stateful(list) # Same as stateful(lambda: [])
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
from dory import DoryApp, BaseProcessor, stateful
|
|
81
|
+
|
|
82
|
+
class MyProcessor(BaseProcessor):
|
|
83
|
+
counter = stateful(0)
|
|
84
|
+
data = stateful(dict)
|
|
85
|
+
|
|
86
|
+
async def run(self):
|
|
87
|
+
while not self.context.is_shutdown_requested():
|
|
88
|
+
self.counter += 1
|
|
89
|
+
await asyncio.sleep(1)
|
|
90
|
+
|
|
91
|
+
# No need to implement get_state() or restore_state()!
|
|
92
|
+
# SDK handles it automatically.
|
|
93
|
+
"""
|
|
94
|
+
return StatefulVar(_default=default)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_stateful_vars(obj: Any) -> dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Get all stateful variables from an object.
|
|
100
|
+
|
|
101
|
+
Used internally by SDK to auto-generate get_state().
|
|
102
|
+
"""
|
|
103
|
+
cls = type(obj)
|
|
104
|
+
if not hasattr(cls, '_stateful_vars'):
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
result = {}
|
|
108
|
+
for name in cls._stateful_vars:
|
|
109
|
+
result[name] = getattr(obj, name)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_stateful_vars(obj: Any, state: dict[str, Any]) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Set stateful variables on an object from state dict.
|
|
116
|
+
|
|
117
|
+
Used internally by SDK to auto-generate restore_state().
|
|
118
|
+
"""
|
|
119
|
+
cls = type(obj)
|
|
120
|
+
if not hasattr(cls, '_stateful_vars'):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
for name in cls._stateful_vars:
|
|
124
|
+
if name in state:
|
|
125
|
+
setattr(obj, name, state[name])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class StatefulMixin:
|
|
129
|
+
"""
|
|
130
|
+
Mixin that provides automatic get_state() and restore_state() for @stateful vars.
|
|
131
|
+
|
|
132
|
+
If a class has @stateful decorated attributes but doesn't override
|
|
133
|
+
get_state()/restore_state(), this mixin provides default implementations.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def _get_stateful_state(self) -> dict:
|
|
137
|
+
"""Get state from @stateful decorated attributes."""
|
|
138
|
+
return get_stateful_vars(self)
|
|
139
|
+
|
|
140
|
+
def _set_stateful_state(self, state: dict) -> None:
|
|
141
|
+
"""Set state to @stateful decorated attributes."""
|
|
142
|
+
set_stateful_vars(self, state)
|
dory/errors/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error classification and handling for Dory SDK.
|
|
3
|
+
|
|
4
|
+
Provides intelligent error classification to determine appropriate recovery strategies.
|
|
5
|
+
|
|
6
|
+
Error types:
|
|
7
|
+
- TRANSIENT: Temporary failures (network, timeout) - retry
|
|
8
|
+
- PERMANENT: Logic errors (validation, not found) - don't retry
|
|
9
|
+
- RESOURCE: Resource exhaustion (memory, disk) - backoff + scale
|
|
10
|
+
- EXTERNAL: External dependency failures - circuit breaker
|
|
11
|
+
- LOGIC: Application logic errors - fix code
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from dory.errors import ErrorClassifier, ErrorType
|
|
15
|
+
|
|
16
|
+
classifier = ErrorClassifier()
|
|
17
|
+
error_type = classifier.classify(exception)
|
|
18
|
+
|
|
19
|
+
if error_type == ErrorType.TRANSIENT:
|
|
20
|
+
# Retry the operation
|
|
21
|
+
await retry_operation()
|
|
22
|
+
elif error_type == ErrorType.EXTERNAL:
|
|
23
|
+
# Use circuit breaker
|
|
24
|
+
await circuit_breaker.call(operation)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .classification import (
|
|
28
|
+
ErrorType,
|
|
29
|
+
ErrorClassifier,
|
|
30
|
+
ClassificationResult,
|
|
31
|
+
RecoveryAction,
|
|
32
|
+
register_error_type,
|
|
33
|
+
)
|
|
34
|
+
from .codes import (
|
|
35
|
+
ErrorCode,
|
|
36
|
+
ErrorDomain,
|
|
37
|
+
ErrorCodeRegistry,
|
|
38
|
+
DoryError,
|
|
39
|
+
# Retry errors
|
|
40
|
+
E_RET_001,
|
|
41
|
+
E_RET_002,
|
|
42
|
+
E_RET_003,
|
|
43
|
+
# Circuit breaker errors
|
|
44
|
+
E_CBR_001,
|
|
45
|
+
E_CBR_002,
|
|
46
|
+
E_CBR_003,
|
|
47
|
+
# Error classification errors
|
|
48
|
+
E_ECL_001,
|
|
49
|
+
E_ECL_002,
|
|
50
|
+
# Golden image errors
|
|
51
|
+
E_GLD_001,
|
|
52
|
+
E_GLD_002,
|
|
53
|
+
E_GLD_003,
|
|
54
|
+
E_GLD_004,
|
|
55
|
+
E_GLD_005,
|
|
56
|
+
# Validation errors
|
|
57
|
+
E_VAL_001,
|
|
58
|
+
E_VAL_002,
|
|
59
|
+
E_VAL_003,
|
|
60
|
+
# Processing mode errors
|
|
61
|
+
E_MOD_001,
|
|
62
|
+
E_MOD_002,
|
|
63
|
+
E_MOD_003,
|
|
64
|
+
# Request tracking errors
|
|
65
|
+
E_REQ_001,
|
|
66
|
+
E_REQ_002,
|
|
67
|
+
# Connection errors
|
|
68
|
+
E_CON_001,
|
|
69
|
+
E_CON_002,
|
|
70
|
+
E_CON_003,
|
|
71
|
+
# State management errors
|
|
72
|
+
E_STA_001,
|
|
73
|
+
E_STA_002,
|
|
74
|
+
E_STA_003,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
# Classification
|
|
79
|
+
"ErrorType",
|
|
80
|
+
"ErrorClassifier",
|
|
81
|
+
"ClassificationResult",
|
|
82
|
+
"RecoveryAction",
|
|
83
|
+
"register_error_type",
|
|
84
|
+
# Error codes
|
|
85
|
+
"ErrorCode",
|
|
86
|
+
"ErrorDomain",
|
|
87
|
+
"ErrorCodeRegistry",
|
|
88
|
+
"DoryError",
|
|
89
|
+
# Specific error codes
|
|
90
|
+
"E_RET_001",
|
|
91
|
+
"E_RET_002",
|
|
92
|
+
"E_RET_003",
|
|
93
|
+
"E_CBR_001",
|
|
94
|
+
"E_CBR_002",
|
|
95
|
+
"E_CBR_003",
|
|
96
|
+
"E_ECL_001",
|
|
97
|
+
"E_ECL_002",
|
|
98
|
+
"E_GLD_001",
|
|
99
|
+
"E_GLD_002",
|
|
100
|
+
"E_GLD_003",
|
|
101
|
+
"E_GLD_004",
|
|
102
|
+
"E_GLD_005",
|
|
103
|
+
"E_VAL_001",
|
|
104
|
+
"E_VAL_002",
|
|
105
|
+
"E_VAL_003",
|
|
106
|
+
"E_MOD_001",
|
|
107
|
+
"E_MOD_002",
|
|
108
|
+
"E_MOD_003",
|
|
109
|
+
"E_REQ_001",
|
|
110
|
+
"E_REQ_002",
|
|
111
|
+
"E_CON_001",
|
|
112
|
+
"E_CON_002",
|
|
113
|
+
"E_CON_003",
|
|
114
|
+
"E_STA_001",
|
|
115
|
+
"E_STA_002",
|
|
116
|
+
"E_STA_003",
|
|
117
|
+
]
|