dory-sdk 2.1.0__py3-none-any.whl → 2.1.4__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 +32 -1
- dory/config/defaults.py +6 -0
- dory/config/schema.py +26 -0
- dory/edge/__init__.py +88 -0
- dory/edge/adaptive.py +648 -0
- dory/edge/detector.py +546 -0
- dory/edge/fencing.py +488 -0
- dory/edge/heartbeat.py +598 -0
- dory/edge/role.py +416 -0
- dory/health/server.py +283 -9
- dory/k8s/__init__.py +69 -0
- dory/k8s/labels.py +505 -0
- dory/migration/__init__.py +49 -0
- dory/migration/s3_store.py +656 -0
- dory/migration/state_manager.py +64 -6
- dory/migration/transfer.py +382 -0
- dory/migration/versioning.py +749 -0
- {dory_sdk-2.1.0.dist-info → dory_sdk-2.1.4.dist-info}/METADATA +37 -32
- {dory_sdk-2.1.0.dist-info → dory_sdk-2.1.4.dist-info}/RECORD +22 -15
- dory_sdk-2.1.4.dist-info/entry_points.txt +2 -0
- dory/sidecar/__init__.py +0 -6
- dory/sidecar/main.py +0 -75
- dory/sidecar/server.py +0 -329
- dory_sdk-2.1.0.dist-info/entry_points.txt +0 -3
- {dory_sdk-2.1.0.dist-info → dory_sdk-2.1.4.dist-info}/WHEEL +0 -0
- {dory_sdk-2.1.0.dist-info → dory_sdk-2.1.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dory-sdk
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Python SDK for building stateful processors with zero-downtime migration, auto-initialization, and smart instrumentation
|
|
5
5
|
Author-email: Dory Team <dory@example.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -43,9 +43,22 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
43
43
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
44
44
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
45
45
|
Provides-Extra: production
|
|
46
|
-
Requires-Dist:
|
|
46
|
+
Requires-Dist: kubernetes>=28.0.0; extra == "production"
|
|
47
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == "production"
|
|
48
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "production"
|
|
49
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "production"
|
|
47
50
|
Provides-Extra: all
|
|
48
|
-
Requires-Dist:
|
|
51
|
+
Requires-Dist: kubernetes>=28.0.0; extra == "all"
|
|
52
|
+
Requires-Dist: boto3>=1.28.0; extra == "all"
|
|
53
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == "all"
|
|
54
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "all"
|
|
55
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "all"
|
|
56
|
+
Requires-Dist: pytest>=7.0.0; extra == "all"
|
|
57
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
58
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "all"
|
|
59
|
+
Requires-Dist: mypy>=1.0.0; extra == "all"
|
|
60
|
+
Requires-Dist: ruff>=0.1.0; extra == "all"
|
|
61
|
+
Requires-Dist: black>=23.0.0; extra == "all"
|
|
49
62
|
|
|
50
63
|
# Dory SDK for Python
|
|
51
64
|
|
|
@@ -129,7 +142,7 @@ if __name__ == "__main__":
|
|
|
129
142
|
## Installation
|
|
130
143
|
|
|
131
144
|
```bash
|
|
132
|
-
pip install dory-sdk[
|
|
145
|
+
pip install dory-sdk[production]
|
|
133
146
|
```
|
|
134
147
|
|
|
135
148
|
---
|
|
@@ -175,17 +188,7 @@ With values file:
|
|
|
175
188
|
helm install my-app ./helm/dory-processor -f values.yaml
|
|
176
189
|
```
|
|
177
190
|
|
|
178
|
-
### Option 2:
|
|
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
|
|
191
|
+
### Option 2: CLI Generated Manifests
|
|
189
192
|
|
|
190
193
|
```bash
|
|
191
194
|
dory init my-app --image my-app:latest
|
|
@@ -197,8 +200,8 @@ kubectl apply -f k8s/
|
|
|
197
200
|
| Choose | When |
|
|
198
201
|
|--------|------|
|
|
199
202
|
| **Helm** | Need release management, rollback, existing Helm workflow |
|
|
200
|
-
| **Kustomize** | GitOps (ArgoCD/Flux), prefer patch-based config |
|
|
201
203
|
| **CLI** | Quick start, simple deployments |
|
|
204
|
+
| **Orchestrator** | Production deployments with dynamic pod management |
|
|
202
205
|
|
|
203
206
|
---
|
|
204
207
|
|
|
@@ -225,10 +228,9 @@ helm install my-app ./helm/dory-processor \
|
|
|
225
228
|
--set sidecar.enabled=true
|
|
226
229
|
```
|
|
227
230
|
|
|
228
|
-
**Using
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```
|
|
231
|
+
**Using Orchestrator:**
|
|
232
|
+
|
|
233
|
+
Configure `use_sidecar: true` in your `runtime_config_template` - the orchestrator automatically injects the sidecar container.
|
|
232
234
|
|
|
233
235
|
### How It Works
|
|
234
236
|
|
|
@@ -262,8 +264,11 @@ The sidecar:
|
|
|
262
264
|
|
|
263
265
|
### Build the Sidecar Image
|
|
264
266
|
|
|
267
|
+
The sidecar is maintained in the [orchestrator repository](../orchestrator/sidecar/). See the sidecar README for build and push instructions.
|
|
268
|
+
|
|
265
269
|
```bash
|
|
266
|
-
|
|
270
|
+
cd ../orchestrator/sidecar
|
|
271
|
+
docker build --platform linux/amd64 -t dory-sidecar:latest .
|
|
267
272
|
```
|
|
268
273
|
|
|
269
274
|
---
|
|
@@ -273,7 +278,7 @@ docker build -f Dockerfile.sidecar -t dory-sidecar:1.0.0 .
|
|
|
273
278
|
### Step 1: Install SDK
|
|
274
279
|
|
|
275
280
|
```bash
|
|
276
|
-
pip install dory-sdk[
|
|
281
|
+
pip install dory-sdk[production]
|
|
277
282
|
```
|
|
278
283
|
|
|
279
284
|
### Step 2: Write Your Processor
|
|
@@ -314,13 +319,7 @@ kubectl apply -f k8s/
|
|
|
314
319
|
helm install my-app ./helm/dory-processor --set image.repository=my-app
|
|
315
320
|
```
|
|
316
321
|
|
|
317
|
-
**Option C:
|
|
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**
|
|
322
|
+
**Option C: Manual Setup**
|
|
324
323
|
|
|
325
324
|
1. Create RBAC:
|
|
326
325
|
```yaml
|
|
@@ -646,17 +645,23 @@ if __name__ == "__main__":
|
|
|
646
645
|
|
|
647
646
|
## Documentation
|
|
648
647
|
|
|
649
|
-
|
|
648
|
+
| Guide | Description |
|
|
649
|
+
|-------|-------------|
|
|
650
|
+
| [Quick Reference](docs/QUICK_REFERENCE.md) | 15-minute quick start |
|
|
651
|
+
| [Configuration Guide](docs/CONFIGURATION_GUIDE.md) | Configuration and presets |
|
|
652
|
+
| [Stateful Guide](docs/STATEFUL_GUIDE.md) | Stateful processing patterns |
|
|
653
|
+
| [Developer Guide](docs/DEVELOPER_GUIDE.md) | Advanced topics |
|
|
650
654
|
|
|
651
655
|
## Examples
|
|
652
656
|
|
|
653
657
|
| Example | Description | Pattern |
|
|
654
658
|
|---------|-------------|---------|
|
|
659
|
+
| [`dory-cloud-processor-py`](examples/dory-cloud-processor-py/) | **Complete SDK demo for cloud/managed nodes** | All SDK features: @stateful, CircuitBreaker, retry, OpenTelemetry, recovery |
|
|
660
|
+
| [`dory-edge-processor-py`](examples/dory-edge-processor-py/) | **Complete SDK demo for edge nodes** | Edge features: FencingManager, HeartbeatManager, RoleManager, failover |
|
|
655
661
|
| [`minimal-processor-py`](../examples/minimal-processor-py/) | Simplest possible processor (~95 lines) | `@stateful` + `run_loop()` |
|
|
656
662
|
| [`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
663
|
|
|
659
|
-
**Start here**: Use `
|
|
664
|
+
**Start here**: Use `dory-cloud-processor-py` for cloud deployments or `dory-edge-processor-py` for edge deployments. Both demonstrate ALL SDK capabilities.
|
|
660
665
|
|
|
661
666
|
## License
|
|
662
667
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
dory/__init__.py,sha256=
|
|
1
|
+
dory/__init__.py,sha256=uVblQcWaULMmrqpOduepDwljKYlw5xJedP0r8vVGRkQ,2267
|
|
2
2
|
dory/auto_instrument.py,sha256=8vKhOi0nXFuG0DN7KIFC20rzfNOZtbl0oAMxFvXd3hA,4689
|
|
3
3
|
dory/decorators.py,sha256=q2eCjlP4LaTjpmJZt30pzx1J1mhuoQvpm4JrHkq8IsY,4384
|
|
4
4
|
dory/py.typed,sha256=emnf6qe_Z56R1XIXFqsN16wUecXbya2eYibnwMfH2Ng,93
|
|
@@ -8,10 +8,10 @@ dory/cli/__init__.py,sha256=gz5g4VP5enbVTadWqqS_1Ys8-xMqPEmGB8AerouysVc,78
|
|
|
8
8
|
dory/cli/main.py,sha256=c2bXolTE8wLs9PU4t9bg9SNPu4RojQ3M_0W54TWQvI4,7676
|
|
9
9
|
dory/cli/templates.py,sha256=TvBUobvYwh20rTsMb3zbUvdXB57gH_OyA5KonWdD_Go,7820
|
|
10
10
|
dory/config/__init__.py,sha256=mGgsgkFclIVyKs69kKZzRYe2FjC3JtzD9etIQvzLLRI,530
|
|
11
|
-
dory/config/defaults.py,sha256=
|
|
11
|
+
dory/config/defaults.py,sha256=IFK5JfQtTr27duPLTQGT8WJhNUkBUIoqt0uAQDZbrSY,1510
|
|
12
12
|
dory/config/loader.py,sha256=hQO37Sk1EYWbVD1E_Mb0FIjHRNupjiNoTf83CrhIe6I,11783
|
|
13
13
|
dory/config/presets.py,sha256=SIGFLjfE4R39RLcX4PxH6eW7ewtnC5vlP3DO8y6bVsk,7848
|
|
14
|
-
dory/config/schema.py,sha256=
|
|
14
|
+
dory/config/schema.py,sha256=npSD9qXC9WMDPhh2s8rx25c2AXrPUVJ462psVbB6NqE,5610
|
|
15
15
|
dory/core/__init__.py,sha256=qN3cpZT2yqCbo_D7dcJYsLErY4LI-v1Na2ULq_wLt1g,614
|
|
16
16
|
dory/core/app.py,sha256=SVTqVgtCYx629cmwh8nebBACKekX3sJJHQarza0YXBw,14173
|
|
17
17
|
dory/core/context.py,sha256=92tIPIG4qCYRAyk0i-bFw2tZxY_1E1rFAJ82cSNIIhQ,7186
|
|
@@ -20,15 +20,22 @@ dory/core/meta.py,sha256=AkTzGrnClY75BIYPYTn7IpZKTvg5MV5-rprHjYC9Xu0,3827
|
|
|
20
20
|
dory/core/modes.py,sha256=KpJioN170NyUfrKhyHPhAnNx4GP5inf57xetYBQeKLg,16658
|
|
21
21
|
dory/core/processor.py,sha256=rjHRk-uSfmypKp0XbRrJLPVHo06QwdPaMRyvQs3Z_VM,24180
|
|
22
22
|
dory/core/signals.py,sha256=cwsjvbKPT_-iyx6I2cKM8XedreEULfpUpfVoV1LTBx0,4205
|
|
23
|
+
dory/edge/__init__.py,sha256=3EbWFzvaAbXRTboTynMiN9mh_D2pgqdIwNloOl7PwOI,1927
|
|
24
|
+
dory/edge/adaptive.py,sha256=Dc4aRNEfe7fi37hJHeZ5HtmOcidItZXXMBsIxGg9BAw,22693
|
|
25
|
+
dory/edge/detector.py,sha256=qcg4EghVRNdELa-d29ryDs48Otxp23pkW9Ed5DvMp5E,17602
|
|
26
|
+
dory/edge/fencing.py,sha256=1hfeV8bPvsqF-9LJ-ZVOkVL_n_2Li1dpRllnJSXIsIc,15557
|
|
27
|
+
dory/edge/heartbeat.py,sha256=74JR-RzQOv6Ok_F4h3HXMGo0YmJ_VcZtjYxkncnFklk,20227
|
|
28
|
+
dory/edge/role.py,sha256=RrJ1wUCOb5XkobHTs_SYuH_UWqI1NX5ZVaBWDkblLU0,12850
|
|
23
29
|
dory/errors/__init__.py,sha256=GgQfZ5XIrUvcbcOMq3iCQbO0S9SktV-t6lTRwLrvPzw,2440
|
|
24
30
|
dory/errors/classification.py,sha256=WDXBKisd6rIkvXx13uo7G8PL7pf--6F07f7pELmvQyI,10826
|
|
25
31
|
dory/errors/codes.py,sha256=0FIPSeBC-D0grMWMyPi5zCoojCTOVrIHuFFLNKi2dJw,14922
|
|
26
32
|
dory/health/__init__.py,sha256=qBkKWmQrWAP7gIMlcYG2Rpu91BAZDDfS_rNCeOh8D0Y,228
|
|
27
33
|
dory/health/probes.py,sha256=x4eLqj56Pm1RdqUEAwYlafA_UkjICFuoRPAtKAmj1Gg,6355
|
|
28
|
-
dory/health/server.py,sha256=
|
|
29
|
-
dory/k8s/__init__.py,sha256=
|
|
34
|
+
dory/health/server.py,sha256=6qYqA1OaNcIL7UwI5zUryqOV26dhsz5u7JZXXveE6Cw,21349
|
|
35
|
+
dory/k8s/__init__.py,sha256=q1tshZhemvo-9Yx3vrjsjuwAFDpZAfiVMRwHtvkSxMQ,1804
|
|
30
36
|
dory/k8s/annotation_watcher.py,sha256=jokUaU1sXMEHG1OO6E6IvoE_9nU9F9fPgbACskiv7b8,6019
|
|
31
37
|
dory/k8s/client.py,sha256=5qeortOayciLGYkM1eqNLYDt1nZBDMrvLfRvtHfEKy0,6682
|
|
38
|
+
dory/k8s/labels.py,sha256=kykB9oFhpOHCZECnR6wsjg24pfo4SqbtA40k_NRhL7E,16480
|
|
32
39
|
dory/k8s/pod_metadata.py,sha256=GdbKwftBeJ7lRA69s8qjs1Qrz0Mx5dRpgdN3lhgl2pk,5487
|
|
33
40
|
dory/logging/__init__.py,sha256=rvb6Gdc2KzhwtEOSiKz8_kgzW2oztMLX4zHivkm2fkQ,193
|
|
34
41
|
dory/logging/logger.py,sha256=L7gYdgZQgJBscQWneIm7g4Rk742ImxpISWCfD7Pl7X4,4942
|
|
@@ -38,10 +45,13 @@ dory/middleware/__init__.py,sha256=LyklxfNSIWMDnayLLykU7IBkp5rAXD5UV2XPEZlfjNY,7
|
|
|
38
45
|
dory/middleware/connection_tracker.py,sha256=gHadUF9EP3_sGt1Pp8rerytoLaDpEjIoQq50ZJb-s3U,19154
|
|
39
46
|
dory/middleware/request_id.py,sha256=yfj92c87pJEO1p00_5xM_O4ShQPAQHvM_X7-Kmq87c0,9038
|
|
40
47
|
dory/middleware/request_tracker.py,sha256=h9NqPQXcIgRUPMZC4amn4wJCxgyrq-bAqd3viY0wy1w,16004
|
|
41
|
-
dory/migration/__init__.py,sha256=
|
|
48
|
+
dory/migration/__init__.py,sha256=Ig9wVbLIFnFklQPzsN-xR_1wqtSSw2W0owA0gTxjHgs,1549
|
|
42
49
|
dory/migration/configmap.py,sha256=lNjbgURPuNnNiyBaRHQHBfmuwOLte7JRMeJyqNEpd1k,7680
|
|
50
|
+
dory/migration/s3_store.py,sha256=4u_HXl7rbuo3e9_gSIGgT7icqsmBW6StmBG8I-LlQms,21596
|
|
43
51
|
dory/migration/serialization.py,sha256=REMTknL_efjBt-YKY8DvSt-dDBZdkEfrVaAIn_wtPTk,4773
|
|
44
|
-
dory/migration/state_manager.py,sha256=
|
|
52
|
+
dory/migration/state_manager.py,sha256=YseDpzHS7sUfWAO9bqzB-cPBnEBz7DYZ6GVNTyc24-I,12507
|
|
53
|
+
dory/migration/transfer.py,sha256=WDw02y4N50-j8tpJhSp1wR6hVCa9vg1i6nHLQSuq4HI,11906
|
|
54
|
+
dory/migration/versioning.py,sha256=V-T7xKpXdPI0KqknwmWrnhGqm7sfDxGUyi0AhBFKMPM,24396
|
|
45
55
|
dory/monitoring/__init__.py,sha256=q1MNgYySwpjBvdJnE8BFPdagit_zCdKNplyj6N5TTYU,432
|
|
46
56
|
dory/monitoring/opentelemetry.py,sha256=mdbqPaLVTUItOOhbSTKExyUFwfxi8CtjEkYtzjALyhA,13614
|
|
47
57
|
dory/recovery/__init__.py,sha256=PsSrR8kM1hAzVtb6aFw0ub1jMxd-TXxaEwJf3MhXNWc,1641
|
|
@@ -55,15 +65,12 @@ dory/recovery/state_validator.py,sha256=hE2M6buciiTctUVbV7acE11zXGrXSg2gHPIGOUvG
|
|
|
55
65
|
dory/resilience/__init__.py,sha256=5SxTG4SGEMImLu4_PTjAk1ZY_kaisbkhq0g6speDdzU,945
|
|
56
66
|
dory/resilience/circuit_breaker.py,sha256=blSUfUqyNLWcFNuB1dwQKQMzEWLjujs7qNVaaZidgjc,14920
|
|
57
67
|
dory/resilience/retry.py,sha256=c5Gcg7koRAmMVWYxpHftxNqsOprkfzaZamxnTVZLoA0,12994
|
|
58
|
-
dory/sidecar/__init__.py,sha256=eyAvtD5suu-4TlLrJy5wfC42wtAKFTVKeBQMZKp40Z0,197
|
|
59
|
-
dory/sidecar/main.py,sha256=QXNqa3ulbaZ2qpVQnDCFZ845DgrCxQbH6PbrvzxWnDs,2028
|
|
60
|
-
dory/sidecar/server.py,sha256=eAizFIJp9aRpN_uwt9ylWd5uuISmEaDslSrDm9SGHk8,12054
|
|
61
68
|
dory/utils/__init__.py,sha256=44QY6SZdUq3Ht27k7e8LMpEArcWAoixPfchH0aRROgc,530
|
|
62
69
|
dory/utils/errors.py,sha256=K3ofXRoTmGpXQ1WN836ut-WpvmT70obxRkNiYCk3EO0,1312
|
|
63
70
|
dory/utils/retry.py,sha256=WVrEEkSF2NW8RM1oRv3sKWSJkwVdh4BPXvu7jUPcPHo,3055
|
|
64
71
|
dory/utils/timeout.py,sha256=pDHpO1OfH9hfi8_-gUK2mEXTXb1outsTdAobO3jxbzs,2059
|
|
65
|
-
dory_sdk-2.1.
|
|
66
|
-
dory_sdk-2.1.
|
|
67
|
-
dory_sdk-2.1.
|
|
68
|
-
dory_sdk-2.1.
|
|
69
|
-
dory_sdk-2.1.
|
|
72
|
+
dory_sdk-2.1.4.dist-info/METADATA,sha256=5w2ObfxKEQUcjvMDmKMymz6tkE4vOYn1y-u71n5B7w0,17930
|
|
73
|
+
dory_sdk-2.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
74
|
+
dory_sdk-2.1.4.dist-info/entry_points.txt,sha256=JtdjTIlmVvGH9F2TIPwQGtdxZOyv8xkxJSF9KeDT5UU,44
|
|
75
|
+
dory_sdk-2.1.4.dist-info/top_level.txt,sha256=lbgLxVkyGEP63BrBq53S9m0Mv7t-ZP-B0WHKTmOpOz0,5
|
|
76
|
+
dory_sdk-2.1.4.dist-info/RECORD,,
|
dory/sidecar/__init__.py
DELETED
dory/sidecar/main.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""Entry point for the Dory sidecar."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import signal
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
from dory.sidecar.server import SidecarServer, SidecarConfig
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def setup_logging() -> None:
|
|
13
|
-
"""Configure logging for the sidecar."""
|
|
14
|
-
log_level = os.getenv("DORY_LOG_LEVEL", "INFO").upper()
|
|
15
|
-
log_format = os.getenv("DORY_LOG_FORMAT", "text")
|
|
16
|
-
|
|
17
|
-
if log_format == "json":
|
|
18
|
-
import json
|
|
19
|
-
|
|
20
|
-
class JsonFormatter(logging.Formatter):
|
|
21
|
-
def format(self, record):
|
|
22
|
-
return json.dumps({
|
|
23
|
-
"timestamp": self.formatTime(record),
|
|
24
|
-
"level": record.levelname,
|
|
25
|
-
"logger": record.name,
|
|
26
|
-
"message": record.getMessage(),
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
formatter = JsonFormatter()
|
|
30
|
-
else:
|
|
31
|
-
formatter = logging.Formatter(
|
|
32
|
-
"%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
handler = logging.StreamHandler(sys.stdout)
|
|
36
|
-
handler.setFormatter(formatter)
|
|
37
|
-
|
|
38
|
-
root_logger = logging.getLogger()
|
|
39
|
-
root_logger.addHandler(handler)
|
|
40
|
-
root_logger.setLevel(getattr(logging, log_level, logging.INFO))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def run_sidecar() -> None:
|
|
44
|
-
"""Run the sidecar server (CLI entry point)."""
|
|
45
|
-
setup_logging()
|
|
46
|
-
logger = logging.getLogger("dory.sidecar")
|
|
47
|
-
|
|
48
|
-
config = SidecarConfig.from_env()
|
|
49
|
-
server = SidecarServer(config)
|
|
50
|
-
|
|
51
|
-
loop = asyncio.new_event_loop()
|
|
52
|
-
asyncio.set_event_loop(loop)
|
|
53
|
-
|
|
54
|
-
# Handle shutdown signals
|
|
55
|
-
def handle_signal(sig):
|
|
56
|
-
logger.info(f"Received signal {sig}, shutting down...")
|
|
57
|
-
for task in asyncio.all_tasks(loop):
|
|
58
|
-
task.cancel()
|
|
59
|
-
|
|
60
|
-
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
61
|
-
loop.add_signal_handler(sig, lambda s=sig: handle_signal(s))
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
loop.run_until_complete(server.run_forever())
|
|
65
|
-
except asyncio.CancelledError:
|
|
66
|
-
pass
|
|
67
|
-
finally:
|
|
68
|
-
loop.run_until_complete(server.stop())
|
|
69
|
-
loop.close()
|
|
70
|
-
|
|
71
|
-
logger.info("Sidecar shutdown complete")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if __name__ == "__main__":
|
|
75
|
-
run_sidecar()
|
dory/sidecar/server.py
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
"""Sidecar health server that proxies health checks for non-SDK apps."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import time
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
import aiohttp
|
|
11
|
-
from aiohttp import web
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger("dory.sidecar")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass
|
|
17
|
-
class SidecarConfig:
|
|
18
|
-
"""Configuration for the sidecar server."""
|
|
19
|
-
|
|
20
|
-
# Sidecar health server port
|
|
21
|
-
health_port: int = 8080
|
|
22
|
-
|
|
23
|
-
# Main app configuration (optional - for health forwarding)
|
|
24
|
-
app_port: Optional[int] = None
|
|
25
|
-
app_host: str = "localhost"
|
|
26
|
-
app_health_path: Optional[str] = None # e.g., "/health"
|
|
27
|
-
app_ready_path: Optional[str] = None # e.g., "/ready"
|
|
28
|
-
app_prestop_path: Optional[str] = None # e.g., "/shutdown"
|
|
29
|
-
|
|
30
|
-
# Timeouts
|
|
31
|
-
app_check_timeout: float = 2.0
|
|
32
|
-
prestop_timeout: float = 25.0
|
|
33
|
-
|
|
34
|
-
# Behavior
|
|
35
|
-
ready_requires_app: bool = False # If True, /ready fails if app doesn't respond
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def from_env(cls) -> "SidecarConfig":
|
|
39
|
-
"""Load configuration from environment variables."""
|
|
40
|
-
return cls(
|
|
41
|
-
health_port=int(os.getenv("DORY_HEALTH_PORT", "8080")),
|
|
42
|
-
app_port=int(os.getenv("DORY_APP_PORT")) if os.getenv("DORY_APP_PORT") else None,
|
|
43
|
-
app_host=os.getenv("DORY_APP_HOST", "localhost"),
|
|
44
|
-
app_health_path=os.getenv("DORY_APP_HEALTH_PATH"),
|
|
45
|
-
app_ready_path=os.getenv("DORY_APP_READY_PATH"),
|
|
46
|
-
app_prestop_path=os.getenv("DORY_APP_PRESTOP_PATH"),
|
|
47
|
-
app_check_timeout=float(os.getenv("DORY_APP_CHECK_TIMEOUT", "2.0")),
|
|
48
|
-
prestop_timeout=float(os.getenv("DORY_PRESTOP_TIMEOUT", "25.0")),
|
|
49
|
-
ready_requires_app=os.getenv("DORY_READY_REQUIRES_APP", "false").lower() == "true",
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class SidecarServer:
|
|
54
|
-
"""
|
|
55
|
-
Lightweight sidecar server that provides Kubernetes health endpoints.
|
|
56
|
-
|
|
57
|
-
This allows apps without SDK integration to run in Dory by providing
|
|
58
|
-
the required health endpoints. Apps won't get state migration benefits,
|
|
59
|
-
but they will run normally.
|
|
60
|
-
|
|
61
|
-
Features:
|
|
62
|
-
- Always responds to /healthz (sidecar is alive)
|
|
63
|
-
- Optionally checks main app for /ready
|
|
64
|
-
- Optionally calls main app on /prestop
|
|
65
|
-
- Exposes basic /metrics
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
def __init__(self, config: Optional[SidecarConfig] = None):
|
|
69
|
-
self.config = config or SidecarConfig.from_env()
|
|
70
|
-
self._app: Optional[web.Application] = None
|
|
71
|
-
self._runner: Optional[web.AppRunner] = None
|
|
72
|
-
self._start_time = time.time()
|
|
73
|
-
self._ready = True
|
|
74
|
-
self._shutting_down = False
|
|
75
|
-
self._request_count = 0
|
|
76
|
-
self._app_check_failures = 0
|
|
77
|
-
|
|
78
|
-
async def _check_app_endpoint(self, path: str) -> tuple[bool, str]:
|
|
79
|
-
"""Check if the main app responds on the given path."""
|
|
80
|
-
if not self.config.app_port:
|
|
81
|
-
return True, "No app port configured"
|
|
82
|
-
|
|
83
|
-
url = f"http://{self.config.app_host}:{self.config.app_port}{path}"
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
timeout = aiohttp.ClientTimeout(total=self.config.app_check_timeout)
|
|
87
|
-
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
88
|
-
async with session.get(url) as response:
|
|
89
|
-
if response.status < 400:
|
|
90
|
-
return True, f"App responded with {response.status}"
|
|
91
|
-
else:
|
|
92
|
-
return False, f"App responded with {response.status}"
|
|
93
|
-
except asyncio.TimeoutError:
|
|
94
|
-
return False, "App health check timed out"
|
|
95
|
-
except aiohttp.ClientError as e:
|
|
96
|
-
return False, f"App connection failed: {e}"
|
|
97
|
-
except Exception as e:
|
|
98
|
-
return False, f"App check error: {e}"
|
|
99
|
-
|
|
100
|
-
async def _call_app_prestop(self) -> tuple[bool, str]:
|
|
101
|
-
"""Call the main app's prestop endpoint if configured."""
|
|
102
|
-
if not self.config.app_port or not self.config.app_prestop_path:
|
|
103
|
-
return True, "No app prestop path configured"
|
|
104
|
-
|
|
105
|
-
url = f"http://{self.config.app_host}:{self.config.app_port}{self.config.app_prestop_path}"
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
timeout = aiohttp.ClientTimeout(total=self.config.prestop_timeout)
|
|
109
|
-
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
110
|
-
async with session.get(url) as response:
|
|
111
|
-
body = await response.text()
|
|
112
|
-
return response.status < 400, f"App prestop returned {response.status}: {body}"
|
|
113
|
-
except Exception as e:
|
|
114
|
-
return False, f"App prestop failed: {e}"
|
|
115
|
-
|
|
116
|
-
async def handle_healthz(self, request: web.Request) -> web.Response:
|
|
117
|
-
"""
|
|
118
|
-
Liveness probe - returns 200 if sidecar is running.
|
|
119
|
-
|
|
120
|
-
This always succeeds because if we can respond, we're alive.
|
|
121
|
-
The main app's health is checked separately via /ready if configured.
|
|
122
|
-
"""
|
|
123
|
-
self._request_count += 1
|
|
124
|
-
|
|
125
|
-
return web.json_response({
|
|
126
|
-
"status": "healthy",
|
|
127
|
-
"sidecar": True,
|
|
128
|
-
"uptime_seconds": int(time.time() - self._start_time),
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
async def handle_ready(self, request: web.Request) -> web.Response:
|
|
132
|
-
"""
|
|
133
|
-
Readiness probe - optionally checks main app health.
|
|
134
|
-
|
|
135
|
-
Behavior depends on configuration:
|
|
136
|
-
- If app_ready_path is set, checks that endpoint
|
|
137
|
-
- If app_health_path is set (and ready_path not), checks health endpoint
|
|
138
|
-
- If app_port is set (no paths), checks if port is accepting connections
|
|
139
|
-
- If ready_requires_app=true, fails if app check fails
|
|
140
|
-
- Otherwise, always returns ready
|
|
141
|
-
"""
|
|
142
|
-
self._request_count += 1
|
|
143
|
-
|
|
144
|
-
if self._shutting_down:
|
|
145
|
-
return web.json_response(
|
|
146
|
-
{"status": "shutting_down", "ready": False},
|
|
147
|
-
status=503
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Determine which path to check
|
|
151
|
-
check_path = self.config.app_ready_path or self.config.app_health_path
|
|
152
|
-
|
|
153
|
-
app_ok = True
|
|
154
|
-
app_message = "No app check configured"
|
|
155
|
-
|
|
156
|
-
if self.config.app_port:
|
|
157
|
-
if check_path:
|
|
158
|
-
app_ok, app_message = await self._check_app_endpoint(check_path)
|
|
159
|
-
else:
|
|
160
|
-
# Just check if port is open
|
|
161
|
-
try:
|
|
162
|
-
_, writer = await asyncio.wait_for(
|
|
163
|
-
asyncio.open_connection(self.config.app_host, self.config.app_port),
|
|
164
|
-
timeout=self.config.app_check_timeout
|
|
165
|
-
)
|
|
166
|
-
writer.close()
|
|
167
|
-
await writer.wait_closed()
|
|
168
|
-
app_ok = True
|
|
169
|
-
app_message = "App port is accepting connections"
|
|
170
|
-
except Exception as e:
|
|
171
|
-
app_ok = False
|
|
172
|
-
app_message = f"App port not responding: {e}"
|
|
173
|
-
|
|
174
|
-
if not app_ok:
|
|
175
|
-
self._app_check_failures += 1
|
|
176
|
-
|
|
177
|
-
# Decide if we should report not ready
|
|
178
|
-
if not app_ok and self.config.ready_requires_app:
|
|
179
|
-
return web.json_response(
|
|
180
|
-
{
|
|
181
|
-
"status": "not_ready",
|
|
182
|
-
"ready": False,
|
|
183
|
-
"app_status": app_message,
|
|
184
|
-
"sidecar": True,
|
|
185
|
-
},
|
|
186
|
-
status=503
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return web.json_response({
|
|
190
|
-
"status": "ready",
|
|
191
|
-
"ready": True,
|
|
192
|
-
"app_status": app_message,
|
|
193
|
-
"app_ok": app_ok,
|
|
194
|
-
"sidecar": True,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
async def handle_prestop(self, request: web.Request) -> web.Response:
|
|
198
|
-
"""
|
|
199
|
-
PreStop hook handler - signals graceful shutdown.
|
|
200
|
-
|
|
201
|
-
If app_prestop_path is configured, calls that endpoint to give
|
|
202
|
-
the main app a chance to clean up. Otherwise just marks as shutting down.
|
|
203
|
-
|
|
204
|
-
Note: Without SDK integration, no state will be saved.
|
|
205
|
-
"""
|
|
206
|
-
self._request_count += 1
|
|
207
|
-
self._shutting_down = True
|
|
208
|
-
self._ready = False
|
|
209
|
-
|
|
210
|
-
logger.info("PreStop hook called - beginning graceful shutdown")
|
|
211
|
-
|
|
212
|
-
# Call app's prestop if configured
|
|
213
|
-
app_prestop_ok, app_prestop_message = await self._call_app_prestop()
|
|
214
|
-
|
|
215
|
-
if not app_prestop_ok:
|
|
216
|
-
logger.warning(f"App prestop failed: {app_prestop_message}")
|
|
217
|
-
|
|
218
|
-
return web.json_response({
|
|
219
|
-
"status": "shutting_down",
|
|
220
|
-
"app_prestop": app_prestop_message,
|
|
221
|
-
"state_saved": False, # No state save without SDK
|
|
222
|
-
"sidecar": True,
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
async def handle_metrics(self, request: web.Request) -> web.Response:
|
|
226
|
-
"""Prometheus metrics endpoint."""
|
|
227
|
-
self._request_count += 1
|
|
228
|
-
|
|
229
|
-
uptime = time.time() - self._start_time
|
|
230
|
-
|
|
231
|
-
metrics = f"""# HELP dory_sidecar_up Sidecar is running
|
|
232
|
-
# TYPE dory_sidecar_up gauge
|
|
233
|
-
dory_sidecar_up 1
|
|
234
|
-
|
|
235
|
-
# HELP dory_sidecar_uptime_seconds Sidecar uptime in seconds
|
|
236
|
-
# TYPE dory_sidecar_uptime_seconds gauge
|
|
237
|
-
dory_sidecar_uptime_seconds {uptime:.2f}
|
|
238
|
-
|
|
239
|
-
# HELP dory_sidecar_requests_total Total requests handled
|
|
240
|
-
# TYPE dory_sidecar_requests_total counter
|
|
241
|
-
dory_sidecar_requests_total {self._request_count}
|
|
242
|
-
|
|
243
|
-
# HELP dory_sidecar_app_check_failures_total App health check failures
|
|
244
|
-
# TYPE dory_sidecar_app_check_failures_total counter
|
|
245
|
-
dory_sidecar_app_check_failures_total {self._app_check_failures}
|
|
246
|
-
|
|
247
|
-
# HELP dory_sidecar_shutting_down Sidecar is shutting down
|
|
248
|
-
# TYPE dory_sidecar_shutting_down gauge
|
|
249
|
-
dory_sidecar_shutting_down {1 if self._shutting_down else 0}
|
|
250
|
-
|
|
251
|
-
# HELP dory_sidecar_ready Sidecar ready status
|
|
252
|
-
# TYPE dory_sidecar_ready gauge
|
|
253
|
-
dory_sidecar_ready {1 if self._ready and not self._shutting_down else 0}
|
|
254
|
-
"""
|
|
255
|
-
return web.Response(text=metrics, content_type="text/plain")
|
|
256
|
-
|
|
257
|
-
async def handle_state(self, request: web.Request) -> web.Response:
|
|
258
|
-
"""
|
|
259
|
-
State endpoint - returns empty state for non-SDK apps.
|
|
260
|
-
|
|
261
|
-
This endpoint exists for compatibility but returns no state
|
|
262
|
-
since the app doesn't use the SDK for state management.
|
|
263
|
-
"""
|
|
264
|
-
self._request_count += 1
|
|
265
|
-
|
|
266
|
-
if request.method == "GET":
|
|
267
|
-
return web.json_response({
|
|
268
|
-
"state": None,
|
|
269
|
-
"message": "No state available - app does not use Dory SDK",
|
|
270
|
-
"sidecar": True,
|
|
271
|
-
})
|
|
272
|
-
elif request.method == "POST":
|
|
273
|
-
return web.json_response({
|
|
274
|
-
"restored": False,
|
|
275
|
-
"message": "Cannot restore state - app does not use Dory SDK",
|
|
276
|
-
"sidecar": True,
|
|
277
|
-
})
|
|
278
|
-
else:
|
|
279
|
-
return web.json_response(
|
|
280
|
-
{"error": "Method not allowed"},
|
|
281
|
-
status=405
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
def _create_app(self) -> web.Application:
|
|
285
|
-
"""Create the aiohttp application."""
|
|
286
|
-
app = web.Application()
|
|
287
|
-
|
|
288
|
-
app.router.add_get("/healthz", self.handle_healthz)
|
|
289
|
-
app.router.add_get("/ready", self.handle_ready)
|
|
290
|
-
app.router.add_get("/prestop", self.handle_prestop)
|
|
291
|
-
app.router.add_get("/metrics", self.handle_metrics)
|
|
292
|
-
app.router.add_get("/state", self.handle_state)
|
|
293
|
-
app.router.add_post("/state", self.handle_state)
|
|
294
|
-
|
|
295
|
-
return app
|
|
296
|
-
|
|
297
|
-
async def start(self) -> None:
|
|
298
|
-
"""Start the sidecar server."""
|
|
299
|
-
self._app = self._create_app()
|
|
300
|
-
self._runner = web.AppRunner(self._app)
|
|
301
|
-
await self._runner.setup()
|
|
302
|
-
|
|
303
|
-
site = web.TCPSite(self._runner, "0.0.0.0", self.config.health_port)
|
|
304
|
-
await site.start()
|
|
305
|
-
|
|
306
|
-
logger.info(f"Dory sidecar started on port {self.config.health_port}")
|
|
307
|
-
|
|
308
|
-
if self.config.app_port:
|
|
309
|
-
logger.info(f"Monitoring app on {self.config.app_host}:{self.config.app_port}")
|
|
310
|
-
else:
|
|
311
|
-
logger.info("No app port configured - running in standalone mode")
|
|
312
|
-
|
|
313
|
-
async def stop(self) -> None:
|
|
314
|
-
"""Stop the sidecar server."""
|
|
315
|
-
if self._runner:
|
|
316
|
-
await self._runner.cleanup()
|
|
317
|
-
logger.info("Dory sidecar stopped")
|
|
318
|
-
|
|
319
|
-
async def run_forever(self) -> None:
|
|
320
|
-
"""Run the sidecar server until interrupted."""
|
|
321
|
-
await self.start()
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
while True:
|
|
325
|
-
await asyncio.sleep(3600)
|
|
326
|
-
except asyncio.CancelledError:
|
|
327
|
-
pass
|
|
328
|
-
finally:
|
|
329
|
-
await self.stop()
|
|
File without changes
|
|
File without changes
|