dory-sdk 2.1.2__tar.gz
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_sdk-2.1.2/PKG-INFO +663 -0
- dory_sdk-2.1.2/README.md +614 -0
- dory_sdk-2.1.2/pyproject.toml +175 -0
- dory_sdk-2.1.2/setup.cfg +4 -0
- dory_sdk-2.1.2/src/dory/__init__.py +70 -0
- dory_sdk-2.1.2/src/dory/auto_instrument.py +142 -0
- dory_sdk-2.1.2/src/dory/cli/__init__.py +5 -0
- dory_sdk-2.1.2/src/dory/cli/main.py +290 -0
- dory_sdk-2.1.2/src/dory/cli/templates.py +333 -0
- dory_sdk-2.1.2/src/dory/config/__init__.py +23 -0
- dory_sdk-2.1.2/src/dory/config/defaults.py +50 -0
- dory_sdk-2.1.2/src/dory/config/loader.py +361 -0
- dory_sdk-2.1.2/src/dory/config/presets.py +325 -0
- dory_sdk-2.1.2/src/dory/config/schema.py +152 -0
- dory_sdk-2.1.2/src/dory/core/__init__.py +27 -0
- dory_sdk-2.1.2/src/dory/core/app.py +404 -0
- dory_sdk-2.1.2/src/dory/core/context.py +209 -0
- dory_sdk-2.1.2/src/dory/core/lifecycle.py +214 -0
- dory_sdk-2.1.2/src/dory/core/meta.py +121 -0
- dory_sdk-2.1.2/src/dory/core/modes.py +479 -0
- dory_sdk-2.1.2/src/dory/core/processor.py +654 -0
- dory_sdk-2.1.2/src/dory/core/signals.py +122 -0
- dory_sdk-2.1.2/src/dory/decorators.py +142 -0
- dory_sdk-2.1.2/src/dory/errors/__init__.py +117 -0
- dory_sdk-2.1.2/src/dory/errors/classification.py +362 -0
- dory_sdk-2.1.2/src/dory/errors/codes.py +495 -0
- dory_sdk-2.1.2/src/dory/health/__init__.py +10 -0
- dory_sdk-2.1.2/src/dory/health/probes.py +210 -0
- dory_sdk-2.1.2/src/dory/health/server.py +306 -0
- dory_sdk-2.1.2/src/dory/k8s/__init__.py +11 -0
- dory_sdk-2.1.2/src/dory/k8s/annotation_watcher.py +184 -0
- dory_sdk-2.1.2/src/dory/k8s/client.py +251 -0
- dory_sdk-2.1.2/src/dory/k8s/pod_metadata.py +182 -0
- dory_sdk-2.1.2/src/dory/logging/__init__.py +9 -0
- dory_sdk-2.1.2/src/dory/logging/logger.py +175 -0
- dory_sdk-2.1.2/src/dory/metrics/__init__.py +7 -0
- dory_sdk-2.1.2/src/dory/metrics/collector.py +301 -0
- dory_sdk-2.1.2/src/dory/middleware/__init__.py +36 -0
- dory_sdk-2.1.2/src/dory/middleware/connection_tracker.py +608 -0
- dory_sdk-2.1.2/src/dory/middleware/request_id.py +321 -0
- dory_sdk-2.1.2/src/dory/middleware/request_tracker.py +501 -0
- dory_sdk-2.1.2/src/dory/migration/__init__.py +11 -0
- dory_sdk-2.1.2/src/dory/migration/configmap.py +260 -0
- dory_sdk-2.1.2/src/dory/migration/serialization.py +167 -0
- dory_sdk-2.1.2/src/dory/migration/state_manager.py +301 -0
- dory_sdk-2.1.2/src/dory/monitoring/__init__.py +23 -0
- dory_sdk-2.1.2/src/dory/monitoring/opentelemetry.py +462 -0
- dory_sdk-2.1.2/src/dory/py.typed +2 -0
- dory_sdk-2.1.2/src/dory/recovery/__init__.py +60 -0
- dory_sdk-2.1.2/src/dory/recovery/golden_image.py +480 -0
- dory_sdk-2.1.2/src/dory/recovery/golden_snapshot.py +561 -0
- dory_sdk-2.1.2/src/dory/recovery/golden_validator.py +518 -0
- dory_sdk-2.1.2/src/dory/recovery/partial_recovery.py +479 -0
- dory_sdk-2.1.2/src/dory/recovery/recovery_decision.py +242 -0
- dory_sdk-2.1.2/src/dory/recovery/restart_detector.py +142 -0
- dory_sdk-2.1.2/src/dory/recovery/state_validator.py +187 -0
- dory_sdk-2.1.2/src/dory/resilience/__init__.py +45 -0
- dory_sdk-2.1.2/src/dory/resilience/circuit_breaker.py +454 -0
- dory_sdk-2.1.2/src/dory/resilience/retry.py +389 -0
- dory_sdk-2.1.2/src/dory/sidecar/__init__.py +6 -0
- dory_sdk-2.1.2/src/dory/sidecar/main.py +75 -0
- dory_sdk-2.1.2/src/dory/sidecar/server.py +329 -0
- dory_sdk-2.1.2/src/dory/simple.py +342 -0
- dory_sdk-2.1.2/src/dory/types.py +75 -0
- dory_sdk-2.1.2/src/dory/utils/__init__.py +25 -0
- dory_sdk-2.1.2/src/dory/utils/errors.py +59 -0
- dory_sdk-2.1.2/src/dory/utils/retry.py +115 -0
- dory_sdk-2.1.2/src/dory/utils/timeout.py +80 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/PKG-INFO +663 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/SOURCES.txt +85 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/dependency_links.txt +1 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/entry_points.txt +3 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/requires.txt +32 -0
- dory_sdk-2.1.2/src/dory_sdk.egg-info/top_level.txt +1 -0
- dory_sdk-2.1.2/tests/test_circuit_breaker.py +410 -0
- dory_sdk-2.1.2/tests/test_config.py +148 -0
- dory_sdk-2.1.2/tests/test_decorators.py +245 -0
- dory_sdk-2.1.2/tests/test_error_classification.py +308 -0
- dory_sdk-2.1.2/tests/test_error_codes.py +219 -0
- dory_sdk-2.1.2/tests/test_golden_snapshot.py +308 -0
- dory_sdk-2.1.2/tests/test_health.py +334 -0
- dory_sdk-2.1.2/tests/test_processor.py +178 -0
- dory_sdk-2.1.2/tests/test_recovery.py +230 -0
- dory_sdk-2.1.2/tests/test_request_tracker.py +209 -0
- dory_sdk-2.1.2/tests/test_retry.py +301 -0
- dory_sdk-2.1.2/tests/test_serialization.py +149 -0
- dory_sdk-2.1.2/tests/test_sidecar.py +153 -0
dory_sdk-2.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dory-sdk
|
|
3
|
+
Version: 2.1.2
|
|
4
|
+
Summary: Python SDK for building stateful processors with zero-downtime migration, auto-initialization, and smart instrumentation
|
|
5
|
+
Author-email: Dory Team <dory@example.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/example/dory-sdk
|
|
8
|
+
Project-URL: Documentation, https://dory-sdk.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/example/dory-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/example/dory-sdk/issues
|
|
11
|
+
Keywords: kubernetes,stateful,migration,orchestration,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: PyYAML>=6.0
|
|
28
|
+
Provides-Extra: kubernetes
|
|
29
|
+
Requires-Dist: kubernetes>=28.0.0; extra == "kubernetes"
|
|
30
|
+
Provides-Extra: s3
|
|
31
|
+
Requires-Dist: boto3>=1.28.0; extra == "s3"
|
|
32
|
+
Provides-Extra: tracing
|
|
33
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == "tracing"
|
|
34
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "tracing"
|
|
35
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "tracing"
|
|
36
|
+
Provides-Extra: resilience
|
|
37
|
+
Provides-Extra: monitoring
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
44
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
45
|
+
Provides-Extra: production
|
|
46
|
+
Requires-Dist: dory-sdk[kubernetes,tracing]; extra == "production"
|
|
47
|
+
Provides-Extra: all
|
|
48
|
+
Requires-Dist: dory-sdk[dev,kubernetes,monitoring,resilience,s3,tracing]; extra == "all"
|
|
49
|
+
|
|
50
|
+
# Dory SDK for Python
|
|
51
|
+
|
|
52
|
+
A Python SDK for building stateful processors with **zero-downtime migration**, **graceful shutdown**, and **state transfer** on Kubernetes.
|
|
53
|
+
|
|
54
|
+
## What Does This SDK Do For You?
|
|
55
|
+
|
|
56
|
+
| Feature | Without Dory SDK | With Dory SDK |
|
|
57
|
+
|---------|------------------|---------------|
|
|
58
|
+
| Pod shutdown | App killed, state lost | State saved automatically, restored on new pod |
|
|
59
|
+
| Node maintenance | Downtime, manual intervention | Zero-downtime migration to new node |
|
|
60
|
+
| Crash recovery | Start from scratch | Resume from last checkpoint |
|
|
61
|
+
| Health monitoring | DIY implementation | Built-in `/healthz`, `/ready`, `/metrics` |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick Start (Choose Your Style)
|
|
66
|
+
|
|
67
|
+
### Option A: Minimal (7 lines)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from dory import DoryApp, BaseProcessor, stateful
|
|
71
|
+
|
|
72
|
+
class MyApp(BaseProcessor):
|
|
73
|
+
counter = stateful(0) # Auto-saved and restored!
|
|
74
|
+
|
|
75
|
+
async def run(self):
|
|
76
|
+
async for _ in self.run_loop(interval=1):
|
|
77
|
+
self.counter += 1
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
DoryApp().run(MyApp)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Option B: Function-Based (6 lines)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from dory.simple import processor, state
|
|
87
|
+
|
|
88
|
+
counter = state(0)
|
|
89
|
+
|
|
90
|
+
@processor
|
|
91
|
+
async def main(ctx):
|
|
92
|
+
async for _ in ctx.run_loop(interval=1):
|
|
93
|
+
counter.value += 1
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Option C: Full Control
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from dory import DoryApp, BaseProcessor, ExecutionContext
|
|
100
|
+
|
|
101
|
+
class MyApp(BaseProcessor):
|
|
102
|
+
def __init__(self, context: ExecutionContext):
|
|
103
|
+
super().__init__(context)
|
|
104
|
+
self.counter = 0
|
|
105
|
+
|
|
106
|
+
async def startup(self):
|
|
107
|
+
self.context.logger().info("Starting...")
|
|
108
|
+
|
|
109
|
+
async def run(self):
|
|
110
|
+
while not self.context.is_shutdown_requested():
|
|
111
|
+
self.counter += 1
|
|
112
|
+
await asyncio.sleep(1)
|
|
113
|
+
|
|
114
|
+
async def shutdown(self):
|
|
115
|
+
self.context.logger().info(f"Final count: {self.counter}")
|
|
116
|
+
|
|
117
|
+
def get_state(self):
|
|
118
|
+
return {"counter": self.counter}
|
|
119
|
+
|
|
120
|
+
async def restore_state(self, state):
|
|
121
|
+
self.counter = state.get("counter", 0)
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
DoryApp().run(MyApp)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Installation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install dory-sdk[kubernetes]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## CLI Tool
|
|
138
|
+
|
|
139
|
+
The SDK includes a CLI for generating Kubernetes manifests:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Initialize a new project with all files
|
|
143
|
+
dory init my-app --image my-app:latest
|
|
144
|
+
|
|
145
|
+
# Output:
|
|
146
|
+
# Created main.py
|
|
147
|
+
# Created Dockerfile
|
|
148
|
+
# Created k8s/rbac.yaml
|
|
149
|
+
# Created k8s/deployment.yaml
|
|
150
|
+
|
|
151
|
+
# Generate specific manifests
|
|
152
|
+
dory generate rbac --name my-app
|
|
153
|
+
dory generate deployment --name my-app --image my-app:latest
|
|
154
|
+
dory generate all --name my-app --image my-app:latest
|
|
155
|
+
|
|
156
|
+
# Validate configuration
|
|
157
|
+
dory validate
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Deployment Options
|
|
163
|
+
|
|
164
|
+
### Option 1: Helm Chart
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
helm install my-app ./helm/dory-processor \
|
|
168
|
+
--set name=my-app \
|
|
169
|
+
--set image.repository=my-app \
|
|
170
|
+
--set image.tag=latest
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With values file:
|
|
174
|
+
```bash
|
|
175
|
+
helm install my-app ./helm/dory-processor -f values.yaml
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Option 2: Kustomize
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Deploy to dev
|
|
182
|
+
kubectl apply -k kustomize/overlays/dev
|
|
183
|
+
|
|
184
|
+
# Deploy to production
|
|
185
|
+
kubectl apply -k kustomize/overlays/production
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Option 3: CLI Generated Manifests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
dory init my-app --image my-app:latest
|
|
192
|
+
kubectl apply -f k8s/
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### When to Use Which
|
|
196
|
+
|
|
197
|
+
| Choose | When |
|
|
198
|
+
|--------|------|
|
|
199
|
+
| **Helm** | Need release management, rollback, existing Helm workflow |
|
|
200
|
+
| **Kustomize** | GitOps (ArgoCD/Flux), prefer patch-based config |
|
|
201
|
+
| **CLI** | Quick start, simple deployments |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Sidecar Mode (No SDK Required)
|
|
206
|
+
|
|
207
|
+
Don't want to integrate the SDK? Use **sidecar mode** - your app runs unchanged, a lightweight sidecar handles Kubernetes health endpoints.
|
|
208
|
+
|
|
209
|
+
### What You Get
|
|
210
|
+
|
|
211
|
+
| Feature | With SDK | Sidecar Mode |
|
|
212
|
+
|---------|----------|--------------|
|
|
213
|
+
| Health endpoints | Yes | Yes |
|
|
214
|
+
| Graceful shutdown | Yes | Yes |
|
|
215
|
+
| State migration | Yes | **No** |
|
|
216
|
+
| Zero code changes | No | **Yes** |
|
|
217
|
+
|
|
218
|
+
### Deploy with Sidecar
|
|
219
|
+
|
|
220
|
+
**Using Helm:**
|
|
221
|
+
```bash
|
|
222
|
+
helm install my-app ./helm/dory-processor \
|
|
223
|
+
--set image.repository=your-app \
|
|
224
|
+
--set image.tag=latest \
|
|
225
|
+
--set sidecar.enabled=true
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Using Kustomize:**
|
|
229
|
+
```bash
|
|
230
|
+
kubectl apply -k kustomize/overlays/sidecar
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### How It Works
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
┌─────────────────────────────────────────┐
|
|
237
|
+
│ Pod │
|
|
238
|
+
│ ┌─────────────┐ ┌────────────────┐ │
|
|
239
|
+
│ │ Your App │ │ Dory Sidecar │ │
|
|
240
|
+
│ │ (no SDK) │ │ │ │
|
|
241
|
+
│ │ │ │ /healthz ←────┼──┼── K8s liveness
|
|
242
|
+
│ │ port 8081 ←┼────┼→ /ready ←────┼──┼── K8s readiness
|
|
243
|
+
│ │ │ │ /prestop ←────┼──┼── K8s preStop
|
|
244
|
+
│ │ │ │ /metrics │ │
|
|
245
|
+
│ └─────────────┘ └────────────────┘ │
|
|
246
|
+
└─────────────────────────────────────────┘
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The sidecar:
|
|
250
|
+
- Responds to Kubernetes health probes
|
|
251
|
+
- Optionally monitors your app's port/health endpoint
|
|
252
|
+
- Handles graceful shutdown signals
|
|
253
|
+
|
|
254
|
+
### Sidecar Configuration
|
|
255
|
+
|
|
256
|
+
| Environment Variable | Default | Description |
|
|
257
|
+
|---------------------|---------|-------------|
|
|
258
|
+
| `DORY_APP_PORT` | - | Your app's port (optional monitoring) |
|
|
259
|
+
| `DORY_APP_HEALTH_PATH` | - | Your app's health endpoint to check |
|
|
260
|
+
| `DORY_APP_PRESTOP_PATH` | - | Your app's shutdown endpoint to call |
|
|
261
|
+
| `DORY_READY_REQUIRES_APP` | false | Fail /ready if app doesn't respond |
|
|
262
|
+
|
|
263
|
+
### Build the Sidecar Image
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
docker build -f Dockerfile.sidecar -t dory-sidecar:1.0.0 .
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Integration Guide
|
|
272
|
+
|
|
273
|
+
### Step 1: Install SDK
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
pip install dory-sdk[kubernetes]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Step 2: Write Your Processor
|
|
280
|
+
|
|
281
|
+
**Minimal (with `@stateful`):**
|
|
282
|
+
```python
|
|
283
|
+
from dory import DoryApp, BaseProcessor, stateful
|
|
284
|
+
|
|
285
|
+
class MyApp(BaseProcessor):
|
|
286
|
+
# These are automatically saved/restored
|
|
287
|
+
counter = stateful(0)
|
|
288
|
+
data = stateful(dict)
|
|
289
|
+
|
|
290
|
+
async def run(self):
|
|
291
|
+
async for i in self.run_loop(interval=1):
|
|
292
|
+
self.counter += 1
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
DoryApp().run(MyApp)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**That's it!** The SDK handles:
|
|
299
|
+
- `get_state()` - auto-generated from `@stateful` vars
|
|
300
|
+
- `restore_state()` - auto-generated from `@stateful` vars
|
|
301
|
+
- `startup()` - default no-op
|
|
302
|
+
- `shutdown()` - default no-op
|
|
303
|
+
|
|
304
|
+
### Step 3: Deploy to Kubernetes
|
|
305
|
+
|
|
306
|
+
**Option A: Use CLI**
|
|
307
|
+
```bash
|
|
308
|
+
dory init my-app --image my-app:latest
|
|
309
|
+
kubectl apply -f k8s/
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Option B: Use Helm**
|
|
313
|
+
```bash
|
|
314
|
+
helm install my-app ./helm/dory-processor --set image.repository=my-app
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Option C: Use Kustomize**
|
|
318
|
+
```bash
|
|
319
|
+
# Edit kustomize/overlays/dev/kustomization.yaml to set your image
|
|
320
|
+
kubectl apply -k kustomize/overlays/dev
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Option D: Manual Setup**
|
|
324
|
+
|
|
325
|
+
1. Create RBAC:
|
|
326
|
+
```yaml
|
|
327
|
+
apiVersion: v1
|
|
328
|
+
kind: ServiceAccount
|
|
329
|
+
metadata:
|
|
330
|
+
name: my-app
|
|
331
|
+
---
|
|
332
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
333
|
+
kind: Role
|
|
334
|
+
metadata:
|
|
335
|
+
name: my-app-state-manager
|
|
336
|
+
rules:
|
|
337
|
+
- apiGroups: [""]
|
|
338
|
+
resources: ["configmaps"]
|
|
339
|
+
verbs: ["get", "create", "update", "patch", "delete"]
|
|
340
|
+
---
|
|
341
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
342
|
+
kind: RoleBinding
|
|
343
|
+
metadata:
|
|
344
|
+
name: my-app-state-manager
|
|
345
|
+
subjects:
|
|
346
|
+
- kind: ServiceAccount
|
|
347
|
+
name: my-app
|
|
348
|
+
roleRef:
|
|
349
|
+
kind: Role
|
|
350
|
+
name: my-app-state-manager
|
|
351
|
+
apiGroup: rbac.authorization.k8s.io
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
2. Create Deployment:
|
|
355
|
+
```yaml
|
|
356
|
+
apiVersion: apps/v1
|
|
357
|
+
kind: Deployment
|
|
358
|
+
metadata:
|
|
359
|
+
name: my-app
|
|
360
|
+
spec:
|
|
361
|
+
replicas: 1
|
|
362
|
+
selector:
|
|
363
|
+
matchLabels:
|
|
364
|
+
app: my-app
|
|
365
|
+
template:
|
|
366
|
+
metadata:
|
|
367
|
+
labels:
|
|
368
|
+
app: my-app
|
|
369
|
+
spec:
|
|
370
|
+
serviceAccountName: my-app
|
|
371
|
+
terminationGracePeriodSeconds: 35
|
|
372
|
+
containers:
|
|
373
|
+
- name: my-app
|
|
374
|
+
image: my-app:latest
|
|
375
|
+
env:
|
|
376
|
+
- name: DORY_POD_NAME
|
|
377
|
+
valueFrom:
|
|
378
|
+
fieldRef:
|
|
379
|
+
fieldPath: metadata.name
|
|
380
|
+
- name: DORY_POD_NAMESPACE
|
|
381
|
+
valueFrom:
|
|
382
|
+
fieldRef:
|
|
383
|
+
fieldPath: metadata.namespace
|
|
384
|
+
livenessProbe:
|
|
385
|
+
httpGet:
|
|
386
|
+
path: /healthz
|
|
387
|
+
port: 8080
|
|
388
|
+
readinessProbe:
|
|
389
|
+
httpGet:
|
|
390
|
+
path: /ready
|
|
391
|
+
port: 8080
|
|
392
|
+
lifecycle:
|
|
393
|
+
preStop:
|
|
394
|
+
httpGet:
|
|
395
|
+
path: /prestop
|
|
396
|
+
port: 8080
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Features
|
|
402
|
+
|
|
403
|
+
### `@stateful` Decorator
|
|
404
|
+
|
|
405
|
+
Mark variables for automatic state management:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from dory import BaseProcessor, stateful
|
|
409
|
+
|
|
410
|
+
class MyApp(BaseProcessor):
|
|
411
|
+
# Simple values
|
|
412
|
+
counter = stateful(0)
|
|
413
|
+
name = stateful("default")
|
|
414
|
+
|
|
415
|
+
# Mutable defaults (use factory)
|
|
416
|
+
data = stateful(dict) # Creates new dict for each instance
|
|
417
|
+
items = stateful(list) # Creates new list for each instance
|
|
418
|
+
|
|
419
|
+
async def run(self):
|
|
420
|
+
# Just use them normally - SDK handles save/restore
|
|
421
|
+
self.counter += 1
|
|
422
|
+
self.data["key"] = "value"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `run_loop()` Helper
|
|
426
|
+
|
|
427
|
+
Simplifies the shutdown check pattern:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
# Instead of:
|
|
431
|
+
async def run(self):
|
|
432
|
+
while not self.context.is_shutdown_requested():
|
|
433
|
+
self.counter += 1
|
|
434
|
+
await asyncio.sleep(1)
|
|
435
|
+
|
|
436
|
+
# Use:
|
|
437
|
+
async def run(self):
|
|
438
|
+
async for i in self.run_loop(interval=1):
|
|
439
|
+
self.counter += 1
|
|
440
|
+
print(f"Iteration {i}")
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Function-Based API
|
|
444
|
+
|
|
445
|
+
For simple apps that don't need a class:
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
from dory.simple import processor, state
|
|
449
|
+
|
|
450
|
+
counter = state(0)
|
|
451
|
+
sessions = state(dict)
|
|
452
|
+
|
|
453
|
+
@processor
|
|
454
|
+
async def main(ctx):
|
|
455
|
+
logger = ctx.logger()
|
|
456
|
+
|
|
457
|
+
async for i in ctx.run_loop(interval=1):
|
|
458
|
+
counter.value += 1
|
|
459
|
+
logger.info(f"Count: {counter.value}")
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### ExecutionContext
|
|
463
|
+
|
|
464
|
+
Access pod metadata and utilities:
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
async def run(self):
|
|
468
|
+
ctx = self.context
|
|
469
|
+
|
|
470
|
+
# Logging with pod context
|
|
471
|
+
ctx.logger().info("Processing...")
|
|
472
|
+
|
|
473
|
+
# Pod metadata
|
|
474
|
+
print(f"Pod: {ctx.pod_name}")
|
|
475
|
+
print(f"Namespace: {ctx.pod_namespace}")
|
|
476
|
+
print(f"Processor ID: {ctx.processor_id}")
|
|
477
|
+
print(f"Restart count: {ctx.attempt_number}")
|
|
478
|
+
|
|
479
|
+
# App config (env vars except DORY_*)
|
|
480
|
+
config = ctx.config()
|
|
481
|
+
model_path = config.get("MODEL_PATH")
|
|
482
|
+
|
|
483
|
+
# Shutdown detection
|
|
484
|
+
while not ctx.is_shutdown_requested():
|
|
485
|
+
if ctx.is_migration_imminent():
|
|
486
|
+
print("Migration coming, finishing batch...")
|
|
487
|
+
await process()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Configuration
|
|
493
|
+
|
|
494
|
+
### Environment Variables
|
|
495
|
+
|
|
496
|
+
| Variable | Default | Description |
|
|
497
|
+
|----------|---------|-------------|
|
|
498
|
+
| `DORY_HEALTH_PORT` | 8080 | Health server port |
|
|
499
|
+
| `DORY_LOG_LEVEL` | INFO | Log level |
|
|
500
|
+
| `DORY_LOG_FORMAT` | json | Log format (json/text) |
|
|
501
|
+
| `DORY_STATE_BACKEND` | configmap | State storage backend |
|
|
502
|
+
| `DORY_STARTUP_TIMEOUT_SEC` | 30 | Startup timeout |
|
|
503
|
+
| `DORY_SHUTDOWN_TIMEOUT_SEC` | 30 | Shutdown timeout |
|
|
504
|
+
|
|
505
|
+
### Config File (dory.yaml)
|
|
506
|
+
|
|
507
|
+
```yaml
|
|
508
|
+
health_port: 8080
|
|
509
|
+
log_level: INFO
|
|
510
|
+
log_format: json
|
|
511
|
+
state_backend: configmap
|
|
512
|
+
startup_timeout_sec: 30
|
|
513
|
+
shutdown_timeout_sec: 30
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Local Development
|
|
517
|
+
|
|
518
|
+
Test locally without Kubernetes:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
DORY_STATE_BACKEND=local python main.py
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## HTTP Endpoints
|
|
527
|
+
|
|
528
|
+
| Endpoint | Description |
|
|
529
|
+
|----------|-------------|
|
|
530
|
+
| `GET /healthz` | Liveness probe (200=alive) |
|
|
531
|
+
| `GET /ready` | Readiness probe (200=ready) |
|
|
532
|
+
| `GET /metrics` | Prometheus metrics |
|
|
533
|
+
| `GET /state` | Get processor state |
|
|
534
|
+
| `POST /state` | Restore processor state |
|
|
535
|
+
| `GET /prestop` | PreStop hook handler |
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## API Reference
|
|
540
|
+
|
|
541
|
+
### BaseProcessor Methods
|
|
542
|
+
|
|
543
|
+
| Method | Required | Description |
|
|
544
|
+
|--------|----------|-------------|
|
|
545
|
+
| `run()` | **Yes** | Main processing loop |
|
|
546
|
+
| `startup()` | No | Initialize resources (default: no-op) |
|
|
547
|
+
| `shutdown()` | No | Cleanup resources (default: no-op) |
|
|
548
|
+
| `get_state()` | No | Return state dict (default: `@stateful` vars) |
|
|
549
|
+
| `restore_state(state)` | No | Restore state (default: `@stateful` vars) |
|
|
550
|
+
|
|
551
|
+
### Helper Methods
|
|
552
|
+
|
|
553
|
+
| Method | Description |
|
|
554
|
+
|--------|-------------|
|
|
555
|
+
| `run_loop(interval)` | Async iterator with auto shutdown check |
|
|
556
|
+
| `is_shutting_down()` | Check if shutdown requested |
|
|
557
|
+
|
|
558
|
+
### Fault Handling Hooks (Optional)
|
|
559
|
+
|
|
560
|
+
| Method | Description |
|
|
561
|
+
|--------|-------------|
|
|
562
|
+
| `on_state_restore_failed(error)` | Handle restore errors |
|
|
563
|
+
| `on_rapid_restart_detected(count)` | Handle restart loops |
|
|
564
|
+
| `on_health_check_failed(error)` | Handle health failures |
|
|
565
|
+
| `reset_caches()` | Clear caches on golden reset |
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## How State Migration Works
|
|
570
|
+
|
|
571
|
+
### Pod Shutdown
|
|
572
|
+
```
|
|
573
|
+
1. Kubernetes calls /prestop
|
|
574
|
+
2. SDK saves state to ConfigMap
|
|
575
|
+
3. Pod marked not-ready
|
|
576
|
+
4. Your run() exits
|
|
577
|
+
5. Your shutdown() called
|
|
578
|
+
6. Pod terminates
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### New Pod Startup
|
|
582
|
+
```
|
|
583
|
+
1. SDK finds state in ConfigMap
|
|
584
|
+
2. Your startup() called
|
|
585
|
+
3. Your restore_state() called
|
|
586
|
+
4. Pod marked ready
|
|
587
|
+
5. Your run() starts
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Comparison: Before vs After
|
|
593
|
+
|
|
594
|
+
### Before (25+ lines)
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
import asyncio
|
|
598
|
+
from dory import DoryApp, BaseProcessor, ExecutionContext
|
|
599
|
+
|
|
600
|
+
class MyApp(BaseProcessor):
|
|
601
|
+
def __init__(self, context: ExecutionContext):
|
|
602
|
+
super().__init__(context)
|
|
603
|
+
self.counter = 0
|
|
604
|
+
self.sessions = {}
|
|
605
|
+
|
|
606
|
+
async def startup(self) -> None:
|
|
607
|
+
pass
|
|
608
|
+
|
|
609
|
+
async def run(self) -> None:
|
|
610
|
+
while not self.context.is_shutdown_requested():
|
|
611
|
+
self.counter += 1
|
|
612
|
+
await asyncio.sleep(1)
|
|
613
|
+
|
|
614
|
+
async def shutdown(self) -> None:
|
|
615
|
+
pass
|
|
616
|
+
|
|
617
|
+
def get_state(self) -> dict:
|
|
618
|
+
return {"counter": self.counter, "sessions": self.sessions}
|
|
619
|
+
|
|
620
|
+
async def restore_state(self, state: dict) -> None:
|
|
621
|
+
self.counter = state.get("counter", 0)
|
|
622
|
+
self.sessions = state.get("sessions", {})
|
|
623
|
+
|
|
624
|
+
if __name__ == "__main__":
|
|
625
|
+
DoryApp().run(MyApp)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### After (7 lines)
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
from dory import DoryApp, BaseProcessor, stateful
|
|
632
|
+
|
|
633
|
+
class MyApp(BaseProcessor):
|
|
634
|
+
counter = stateful(0)
|
|
635
|
+
sessions = stateful(dict)
|
|
636
|
+
|
|
637
|
+
async def run(self):
|
|
638
|
+
async for _ in self.run_loop(interval=1):
|
|
639
|
+
self.counter += 1
|
|
640
|
+
|
|
641
|
+
if __name__ == "__main__":
|
|
642
|
+
DoryApp().run(MyApp)
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Documentation
|
|
648
|
+
|
|
649
|
+
- [Developer Guide](docs/DEVELOPER_GUIDE.md) - Advanced topics
|
|
650
|
+
|
|
651
|
+
## Examples
|
|
652
|
+
|
|
653
|
+
| Example | Description | Pattern |
|
|
654
|
+
|---------|-------------|---------|
|
|
655
|
+
| [`minimal-processor-py`](../examples/minimal-processor-py/) | Simplest possible processor (~95 lines) | `@stateful` + `run_loop()` |
|
|
656
|
+
| [`dory-info-logger-py`](../examples/dory-info-logger-py/) | Full demo with HTTP dashboard | `@stateful` + fault hooks |
|
|
657
|
+
| [`dory-edge-logger-py`](../examples/dory-edge-logger-py/) | Edge workload with DB logging | Manual state management |
|
|
658
|
+
|
|
659
|
+
**Start here**: Use `minimal-processor-py` as a template for new processors.
|
|
660
|
+
|
|
661
|
+
## License
|
|
662
|
+
|
|
663
|
+
Apache 2.0
|