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/types.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for Dory SDK.
|
|
3
|
+
|
|
4
|
+
This module contains all type hints and type aliases used across the SDK.
|
|
5
|
+
No application-specific types should be defined here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TypeVar, Callable, Awaitable, Any, Protocol
|
|
9
|
+
from enum import Enum, auto
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LifecycleState(Enum):
|
|
13
|
+
"""States in the processor lifecycle state machine."""
|
|
14
|
+
|
|
15
|
+
CREATED = auto() # Instance created, not yet started
|
|
16
|
+
STARTING = auto() # startup() in progress
|
|
17
|
+
RUNNING = auto() # run() in progress
|
|
18
|
+
SHUTTING_DOWN = auto() # shutdown() in progress
|
|
19
|
+
STOPPED = auto() # Gracefully stopped
|
|
20
|
+
FAILED = auto() # Error occurred
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StateBackend(Enum):
|
|
24
|
+
"""Supported state storage backends."""
|
|
25
|
+
|
|
26
|
+
CONFIGMAP = "configmap" # Kubernetes ConfigMap (default, <1MB)
|
|
27
|
+
PVC = "pvc" # Persistent Volume Claim
|
|
28
|
+
S3 = "s3" # AWS S3 (for multi-cluster)
|
|
29
|
+
LOCAL = "local" # Local file (for testing)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LogFormat(Enum):
|
|
33
|
+
"""Log output formats."""
|
|
34
|
+
|
|
35
|
+
JSON = "json" # Structured JSON (default, for production)
|
|
36
|
+
TEXT = "text" # Human-readable text (for local dev)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RecoveryStrategy(Enum):
|
|
40
|
+
"""Recovery strategies after fault detection."""
|
|
41
|
+
|
|
42
|
+
RESTORE_STATE = "restore_state" # Try to restore from checkpoint
|
|
43
|
+
GOLDEN_IMAGE = "golden_image" # Start fresh, discard state
|
|
44
|
+
GOLDEN_WITH_BACKOFF = "golden_backoff" # Start fresh with delay
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FaultType(Enum):
|
|
48
|
+
"""Types of faults detected by SDK."""
|
|
49
|
+
|
|
50
|
+
CLEAN_EXIT = "clean_exit" # Normal termination
|
|
51
|
+
UNEXPECTED_CRASH = "crash" # Exit code non-zero
|
|
52
|
+
HUNG_PROCESS = "hung" # Liveness probe timeout
|
|
53
|
+
STATE_CORRUPTION = "state_corrupt" # State validation failed
|
|
54
|
+
STARTUP_FAILURE = "startup_fail" # Startup threw exception
|
|
55
|
+
HEALTH_CHECK_FAILURE = "health_fail" # Health endpoint returned error
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Type aliases
|
|
59
|
+
StateDict = dict[str, Any]
|
|
60
|
+
ConfigDict = dict[str, Any]
|
|
61
|
+
MetadataDict = dict[str, str]
|
|
62
|
+
|
|
63
|
+
# Callback types
|
|
64
|
+
ShutdownCallback = Callable[[], Awaitable[None]]
|
|
65
|
+
StateCallback = Callable[[], StateDict]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProcessorProtocol(Protocol):
|
|
69
|
+
"""Protocol defining the processor interface."""
|
|
70
|
+
|
|
71
|
+
async def startup(self) -> None: ...
|
|
72
|
+
async def run(self) -> None: ...
|
|
73
|
+
async def shutdown(self) -> None: ...
|
|
74
|
+
def get_state(self) -> StateDict: ...
|
|
75
|
+
async def restore_state(self, state: StateDict) -> None: ...
|
dory/utils/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Utility modules for Dory SDK."""
|
|
2
|
+
|
|
3
|
+
from dory.utils.errors import (
|
|
4
|
+
DoryError,
|
|
5
|
+
DoryStartupError,
|
|
6
|
+
DoryShutdownError,
|
|
7
|
+
DoryStateError,
|
|
8
|
+
DoryConfigError,
|
|
9
|
+
DoryK8sError,
|
|
10
|
+
)
|
|
11
|
+
from dory.utils.retry import retry_async, RetryConfig
|
|
12
|
+
from dory.utils.timeout import timeout_async, TimeoutConfig
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DoryError",
|
|
16
|
+
"DoryStartupError",
|
|
17
|
+
"DoryShutdownError",
|
|
18
|
+
"DoryStateError",
|
|
19
|
+
"DoryConfigError",
|
|
20
|
+
"DoryK8sError",
|
|
21
|
+
"retry_async",
|
|
22
|
+
"RetryConfig",
|
|
23
|
+
"timeout_async",
|
|
24
|
+
"TimeoutConfig",
|
|
25
|
+
]
|
dory/utils/errors.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dory SDK Exception Classes.
|
|
3
|
+
|
|
4
|
+
All SDK-specific exceptions inherit from DoryError for easy catching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DoryError(Exception):
|
|
9
|
+
"""Base exception for all Dory SDK errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, cause: Exception | None = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.message = message
|
|
14
|
+
self.cause = cause
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
if self.cause:
|
|
18
|
+
return f"{self.message}: {self.cause}"
|
|
19
|
+
return self.message
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DoryStartupError(DoryError):
|
|
23
|
+
"""Raised when processor startup fails."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DoryShutdownError(DoryError):
|
|
28
|
+
"""Raised when processor shutdown fails or times out."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DoryStateError(DoryError):
|
|
33
|
+
"""Raised when state operations fail (snapshot, restore, validation)."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DoryConfigError(DoryError):
|
|
38
|
+
"""Raised when configuration is invalid or missing."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DoryK8sError(DoryError):
|
|
43
|
+
"""Raised when Kubernetes API operations fail."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DoryHealthError(DoryError):
|
|
48
|
+
"""Raised when health check operations fail."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DoryTimeoutError(DoryError):
|
|
53
|
+
"""Raised when an operation times out."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DoryValidationError(DoryError):
|
|
58
|
+
"""Raised when validation fails (state schema, config, etc.)."""
|
|
59
|
+
pass
|
dory/utils/retry.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retry utilities with exponential backoff.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
import random
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Callable, TypeVar, ParamSpec, Awaitable
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
P = ParamSpec("P")
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RetryConfig:
|
|
20
|
+
"""Configuration for retry behavior."""
|
|
21
|
+
|
|
22
|
+
max_attempts: int = 3
|
|
23
|
+
base_delay: float = 1.0 # seconds
|
|
24
|
+
max_delay: float = 60.0 # seconds
|
|
25
|
+
exponential_base: float = 2.0
|
|
26
|
+
jitter: bool = True
|
|
27
|
+
retryable_exceptions: tuple[type[Exception], ...] = field(
|
|
28
|
+
default_factory=lambda: (Exception,)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calculate_delay(
|
|
33
|
+
attempt: int,
|
|
34
|
+
config: RetryConfig,
|
|
35
|
+
) -> float:
|
|
36
|
+
"""Calculate delay for next retry with exponential backoff."""
|
|
37
|
+
delay = config.base_delay * (config.exponential_base ** (attempt - 1))
|
|
38
|
+
delay = min(delay, config.max_delay)
|
|
39
|
+
|
|
40
|
+
if config.jitter:
|
|
41
|
+
# Add random jitter (0-25% of delay)
|
|
42
|
+
delay = delay * (1 + random.uniform(0, 0.25))
|
|
43
|
+
|
|
44
|
+
return delay
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def retry_async(
|
|
48
|
+
func: Callable[P, Awaitable[T]],
|
|
49
|
+
*args: P.args,
|
|
50
|
+
config: RetryConfig | None = None,
|
|
51
|
+
**kwargs: P.kwargs,
|
|
52
|
+
) -> T:
|
|
53
|
+
"""
|
|
54
|
+
Execute an async function with retry logic.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
func: Async function to execute
|
|
58
|
+
*args: Positional arguments for func
|
|
59
|
+
config: Retry configuration (uses defaults if None)
|
|
60
|
+
**kwargs: Keyword arguments for func
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Result of func
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
Last exception if all retries fail
|
|
67
|
+
"""
|
|
68
|
+
if config is None:
|
|
69
|
+
config = RetryConfig()
|
|
70
|
+
|
|
71
|
+
last_exception: Exception | None = None
|
|
72
|
+
|
|
73
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
74
|
+
try:
|
|
75
|
+
return await func(*args, **kwargs)
|
|
76
|
+
except config.retryable_exceptions as e:
|
|
77
|
+
last_exception = e
|
|
78
|
+
|
|
79
|
+
if attempt == config.max_attempts:
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"Retry exhausted after {attempt} attempts: {e}"
|
|
82
|
+
)
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
delay = calculate_delay(attempt, config)
|
|
86
|
+
logger.debug(
|
|
87
|
+
f"Retry attempt {attempt}/{config.max_attempts} failed: {e}. "
|
|
88
|
+
f"Retrying in {delay:.2f}s"
|
|
89
|
+
)
|
|
90
|
+
await asyncio.sleep(delay)
|
|
91
|
+
|
|
92
|
+
if last_exception:
|
|
93
|
+
raise last_exception
|
|
94
|
+
|
|
95
|
+
# Should never reach here, but satisfy type checker
|
|
96
|
+
raise RuntimeError("Retry logic error")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def with_retry(
|
|
100
|
+
config: RetryConfig | None = None,
|
|
101
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
|
102
|
+
"""
|
|
103
|
+
Decorator to add retry logic to async functions.
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
@with_retry(RetryConfig(max_attempts=5))
|
|
107
|
+
async def my_flaky_function():
|
|
108
|
+
...
|
|
109
|
+
"""
|
|
110
|
+
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
111
|
+
@functools.wraps(func)
|
|
112
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
113
|
+
return await retry_async(func, *args, config=config, **kwargs)
|
|
114
|
+
return wrapper
|
|
115
|
+
return decorator
|
dory/utils/timeout.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timeout utilities for async operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Callable, TypeVar, ParamSpec, Awaitable
|
|
9
|
+
|
|
10
|
+
from dory.utils.errors import DoryTimeoutError
|
|
11
|
+
|
|
12
|
+
P = ParamSpec("P")
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TimeoutConfig:
|
|
18
|
+
"""Configuration for timeout behavior."""
|
|
19
|
+
|
|
20
|
+
timeout_seconds: float
|
|
21
|
+
error_message: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def timeout_async(
|
|
25
|
+
func: Callable[P, Awaitable[T]],
|
|
26
|
+
*args: P.args,
|
|
27
|
+
timeout_seconds: float,
|
|
28
|
+
error_message: str | None = None,
|
|
29
|
+
**kwargs: P.kwargs,
|
|
30
|
+
) -> T:
|
|
31
|
+
"""
|
|
32
|
+
Execute an async function with a timeout.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
func: Async function to execute
|
|
36
|
+
*args: Positional arguments for func
|
|
37
|
+
timeout_seconds: Maximum time to wait
|
|
38
|
+
error_message: Custom error message on timeout
|
|
39
|
+
**kwargs: Keyword arguments for func
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Result of func
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
DoryTimeoutError: If function doesn't complete within timeout
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return await asyncio.wait_for(
|
|
49
|
+
func(*args, **kwargs),
|
|
50
|
+
timeout=timeout_seconds,
|
|
51
|
+
)
|
|
52
|
+
except asyncio.TimeoutError:
|
|
53
|
+
msg = error_message or f"Operation timed out after {timeout_seconds}s"
|
|
54
|
+
raise DoryTimeoutError(msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def with_timeout(
|
|
58
|
+
timeout_seconds: float,
|
|
59
|
+
error_message: str | None = None,
|
|
60
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
|
61
|
+
"""
|
|
62
|
+
Decorator to add timeout to async functions.
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
@with_timeout(30, "Startup took too long")
|
|
66
|
+
async def startup():
|
|
67
|
+
...
|
|
68
|
+
"""
|
|
69
|
+
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
72
|
+
return await timeout_async(
|
|
73
|
+
func,
|
|
74
|
+
*args,
|
|
75
|
+
timeout_seconds=timeout_seconds,
|
|
76
|
+
error_message=error_message,
|
|
77
|
+
**kwargs,
|
|
78
|
+
)
|
|
79
|
+
return wrapper
|
|
80
|
+
return decorator
|