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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StateManager - High-level state management for migrations.
|
|
3
|
+
|
|
4
|
+
Provides unified interface for state operations across different
|
|
5
|
+
storage backends (ConfigMap, PVC, S3, local file).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from dory.types import StateBackend
|
|
15
|
+
from dory.migration.serialization import StateSerializer
|
|
16
|
+
from dory.migration.configmap import ConfigMapStore
|
|
17
|
+
from dory.utils.errors import DoryStateError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from dory.config.schema import DoryConfig
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StateManager:
|
|
26
|
+
"""
|
|
27
|
+
High-level state management for processor migrations.
|
|
28
|
+
|
|
29
|
+
Supports multiple backends:
|
|
30
|
+
- ConfigMap: Kubernetes ConfigMap (default, <1MB)
|
|
31
|
+
- PVC: Persistent Volume Claim
|
|
32
|
+
- S3: AWS S3 (for multi-cluster)
|
|
33
|
+
- Local: Local file (for testing)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
LOCAL_STATE_PATH = "/data/dory-state.json"
|
|
37
|
+
LOCAL_STATE_PATH_FALLBACK = "/tmp/dory-state.json"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
backend: str | StateBackend = StateBackend.CONFIGMAP,
|
|
42
|
+
config: "DoryConfig | None" = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize state manager.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
backend: Storage backend to use
|
|
49
|
+
config: SDK configuration
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(backend, str):
|
|
52
|
+
backend = StateBackend(backend)
|
|
53
|
+
|
|
54
|
+
self._backend = backend
|
|
55
|
+
self._config = config
|
|
56
|
+
self._serializer = StateSerializer()
|
|
57
|
+
self._configmap_store: ConfigMapStore | None = None
|
|
58
|
+
|
|
59
|
+
# Get namespace from environment
|
|
60
|
+
self._namespace = os.environ.get("POD_NAMESPACE", "default")
|
|
61
|
+
self._pod_name = os.environ.get("POD_NAME", "unknown")
|
|
62
|
+
|
|
63
|
+
async def save_state(
|
|
64
|
+
self,
|
|
65
|
+
processor_id: str,
|
|
66
|
+
state: dict[str, Any],
|
|
67
|
+
restart_count: int = 0,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Save processor state.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
processor_id: Processor ID
|
|
74
|
+
state: State dictionary to save
|
|
75
|
+
restart_count: Current restart count for metadata
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
DoryStateError: If save fails
|
|
79
|
+
"""
|
|
80
|
+
logger.debug(f"Saving state for processor {processor_id}")
|
|
81
|
+
|
|
82
|
+
# Serialize state
|
|
83
|
+
state_json = self._serializer.serialize(
|
|
84
|
+
state=state,
|
|
85
|
+
processor_id=processor_id,
|
|
86
|
+
pod_name=self._pod_name,
|
|
87
|
+
restart_count=restart_count,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Save to backend
|
|
91
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
92
|
+
await self._save_to_configmap(processor_id, state_json)
|
|
93
|
+
elif self._backend == StateBackend.LOCAL:
|
|
94
|
+
await self._save_to_local(processor_id, state_json)
|
|
95
|
+
elif self._backend == StateBackend.PVC:
|
|
96
|
+
await self._save_to_pvc(processor_id, state_json)
|
|
97
|
+
elif self._backend == StateBackend.S3:
|
|
98
|
+
await self._save_to_s3(processor_id, state_json)
|
|
99
|
+
else:
|
|
100
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
101
|
+
|
|
102
|
+
logger.info(f"State saved for processor {processor_id}")
|
|
103
|
+
|
|
104
|
+
async def load_state(self, processor_id: str) -> dict[str, Any] | None:
|
|
105
|
+
"""
|
|
106
|
+
Load processor state.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
processor_id: Processor ID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
State dictionary, or None if no state found
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
DoryStateError: If load fails
|
|
116
|
+
"""
|
|
117
|
+
logger.debug(f"Loading state for processor {processor_id}")
|
|
118
|
+
|
|
119
|
+
# Load from backend
|
|
120
|
+
state_json: str | None = None
|
|
121
|
+
|
|
122
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
123
|
+
state_json = await self._load_from_configmap(processor_id)
|
|
124
|
+
elif self._backend == StateBackend.LOCAL:
|
|
125
|
+
state_json = await self._load_from_local(processor_id)
|
|
126
|
+
elif self._backend == StateBackend.PVC:
|
|
127
|
+
state_json = await self._load_from_pvc(processor_id)
|
|
128
|
+
elif self._backend == StateBackend.S3:
|
|
129
|
+
state_json = await self._load_from_s3(processor_id)
|
|
130
|
+
else:
|
|
131
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
132
|
+
|
|
133
|
+
if state_json is None:
|
|
134
|
+
logger.debug(f"No state found for processor {processor_id}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Deserialize state
|
|
138
|
+
state = self._serializer.deserialize(state_json)
|
|
139
|
+
logger.info(f"State loaded for processor {processor_id}")
|
|
140
|
+
return state
|
|
141
|
+
|
|
142
|
+
async def delete_state(self, processor_id: str) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Delete processor state (golden image reset).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
processor_id: Processor ID
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if state was deleted
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
DoryStateError: If delete fails
|
|
154
|
+
"""
|
|
155
|
+
logger.debug(f"Deleting state for processor {processor_id}")
|
|
156
|
+
|
|
157
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
158
|
+
return await self._delete_from_configmap(processor_id)
|
|
159
|
+
elif self._backend == StateBackend.LOCAL:
|
|
160
|
+
return await self._delete_from_local(processor_id)
|
|
161
|
+
elif self._backend == StateBackend.PVC:
|
|
162
|
+
return await self._delete_from_pvc(processor_id)
|
|
163
|
+
elif self._backend == StateBackend.S3:
|
|
164
|
+
return await self._delete_from_s3(processor_id)
|
|
165
|
+
else:
|
|
166
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
167
|
+
|
|
168
|
+
# =========================================================================
|
|
169
|
+
# ConfigMap Backend
|
|
170
|
+
# =========================================================================
|
|
171
|
+
|
|
172
|
+
async def _save_to_configmap(self, processor_id: str, state_json: str) -> None:
|
|
173
|
+
"""Save state to Kubernetes ConfigMap."""
|
|
174
|
+
if self._configmap_store is None:
|
|
175
|
+
self._configmap_store = ConfigMapStore(namespace=self._namespace)
|
|
176
|
+
|
|
177
|
+
await self._configmap_store.save(processor_id, state_json)
|
|
178
|
+
|
|
179
|
+
async def _load_from_configmap(self, processor_id: str) -> str | None:
|
|
180
|
+
"""Load state from Kubernetes ConfigMap."""
|
|
181
|
+
if self._configmap_store is None:
|
|
182
|
+
self._configmap_store = ConfigMapStore(namespace=self._namespace)
|
|
183
|
+
|
|
184
|
+
return await self._configmap_store.load(processor_id)
|
|
185
|
+
|
|
186
|
+
async def _delete_from_configmap(self, processor_id: str) -> bool:
|
|
187
|
+
"""Delete state ConfigMap."""
|
|
188
|
+
if self._configmap_store is None:
|
|
189
|
+
self._configmap_store = ConfigMapStore(namespace=self._namespace)
|
|
190
|
+
|
|
191
|
+
return await self._configmap_store.delete(processor_id)
|
|
192
|
+
|
|
193
|
+
# =========================================================================
|
|
194
|
+
# Local File Backend
|
|
195
|
+
# =========================================================================
|
|
196
|
+
|
|
197
|
+
def _get_local_path(self, processor_id: str) -> Path:
|
|
198
|
+
"""Get local file path for state."""
|
|
199
|
+
# Try /data first, fall back to /tmp
|
|
200
|
+
base_path = Path(self.LOCAL_STATE_PATH).parent
|
|
201
|
+
if not base_path.exists():
|
|
202
|
+
base_path = Path(self.LOCAL_STATE_PATH_FALLBACK).parent
|
|
203
|
+
|
|
204
|
+
return base_path / f"dory-state-{processor_id}.json"
|
|
205
|
+
|
|
206
|
+
async def _save_to_local(self, processor_id: str, state_json: str) -> None:
|
|
207
|
+
"""Save state to local file."""
|
|
208
|
+
path = self._get_local_path(processor_id)
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
path.write_text(state_json)
|
|
213
|
+
logger.debug(f"State saved to local file: {path}")
|
|
214
|
+
except Exception as e:
|
|
215
|
+
raise DoryStateError(f"Failed to save state to {path}: {e}", cause=e)
|
|
216
|
+
|
|
217
|
+
async def _load_from_local(self, processor_id: str) -> str | None:
|
|
218
|
+
"""Load state from local file."""
|
|
219
|
+
path = self._get_local_path(processor_id)
|
|
220
|
+
|
|
221
|
+
if not path.exists():
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
return path.read_text()
|
|
226
|
+
except Exception as e:
|
|
227
|
+
raise DoryStateError(f"Failed to load state from {path}: {e}", cause=e)
|
|
228
|
+
|
|
229
|
+
async def _delete_from_local(self, processor_id: str) -> bool:
|
|
230
|
+
"""Delete local state file."""
|
|
231
|
+
path = self._get_local_path(processor_id)
|
|
232
|
+
|
|
233
|
+
if not path.exists():
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
path.unlink()
|
|
238
|
+
logger.debug(f"State file deleted: {path}")
|
|
239
|
+
return True
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise DoryStateError(f"Failed to delete state file {path}: {e}", cause=e)
|
|
242
|
+
|
|
243
|
+
# =========================================================================
|
|
244
|
+
# PVC Backend
|
|
245
|
+
# =========================================================================
|
|
246
|
+
|
|
247
|
+
async def _save_to_pvc(self, processor_id: str, state_json: str) -> None:
|
|
248
|
+
"""Save state to PVC mount."""
|
|
249
|
+
# PVC uses local file at mounted path
|
|
250
|
+
mount_path = self._config.state_pvc_mount if self._config else "/data"
|
|
251
|
+
path = Path(mount_path) / f"dory-state-{processor_id}.json"
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
path.write_text(state_json)
|
|
256
|
+
logger.debug(f"State saved to PVC: {path}")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
raise DoryStateError(f"Failed to save state to PVC {path}: {e}", cause=e)
|
|
259
|
+
|
|
260
|
+
async def _load_from_pvc(self, processor_id: str) -> str | None:
|
|
261
|
+
"""Load state from PVC mount."""
|
|
262
|
+
mount_path = self._config.state_pvc_mount if self._config else "/data"
|
|
263
|
+
path = Path(mount_path) / f"dory-state-{processor_id}.json"
|
|
264
|
+
|
|
265
|
+
if not path.exists():
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
return path.read_text()
|
|
270
|
+
except Exception as e:
|
|
271
|
+
raise DoryStateError(f"Failed to load state from PVC {path}: {e}", cause=e)
|
|
272
|
+
|
|
273
|
+
async def _delete_from_pvc(self, processor_id: str) -> bool:
|
|
274
|
+
"""Delete state from PVC."""
|
|
275
|
+
mount_path = self._config.state_pvc_mount if self._config else "/data"
|
|
276
|
+
path = Path(mount_path) / f"dory-state-{processor_id}.json"
|
|
277
|
+
|
|
278
|
+
if not path.exists():
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
path.unlink()
|
|
283
|
+
return True
|
|
284
|
+
except Exception as e:
|
|
285
|
+
raise DoryStateError(f"Failed to delete state from PVC {path}: {e}", cause=e)
|
|
286
|
+
|
|
287
|
+
# =========================================================================
|
|
288
|
+
# S3 Backend (placeholder - would need boto3)
|
|
289
|
+
# =========================================================================
|
|
290
|
+
|
|
291
|
+
async def _save_to_s3(self, processor_id: str, state_json: str) -> None:
|
|
292
|
+
"""Save state to S3."""
|
|
293
|
+
raise DoryStateError("S3 backend not yet implemented")
|
|
294
|
+
|
|
295
|
+
async def _load_from_s3(self, processor_id: str) -> str | None:
|
|
296
|
+
"""Load state from S3."""
|
|
297
|
+
raise DoryStateError("S3 backend not yet implemented")
|
|
298
|
+
|
|
299
|
+
async def _delete_from_s3(self, processor_id: str) -> bool:
|
|
300
|
+
"""Delete state from S3."""
|
|
301
|
+
raise DoryStateError("S3 backend not yet implemented")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dory Monitoring and Observability
|
|
3
|
+
|
|
4
|
+
OpenTelemetry integration for distributed tracing and metrics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dory.monitoring.opentelemetry import (
|
|
8
|
+
OpenTelemetryManager,
|
|
9
|
+
trace_function,
|
|
10
|
+
create_span,
|
|
11
|
+
add_span_attributes,
|
|
12
|
+
record_exception,
|
|
13
|
+
get_tracer,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"OpenTelemetryManager",
|
|
18
|
+
"trace_function",
|
|
19
|
+
"create_span",
|
|
20
|
+
"add_span_attributes",
|
|
21
|
+
"record_exception",
|
|
22
|
+
"get_tracer",
|
|
23
|
+
]
|