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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dory-sdk
3
- Version: 2.1.0
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: dory-sdk[kubernetes,tracing]; extra == "production"
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: dory-sdk[dev,kubernetes,monitoring,resilience,s3,tracing]; extra == "all"
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[kubernetes]
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: 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
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 Kustomize:**
229
- ```bash
230
- kubectl apply -k kustomize/overlays/sidecar
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
- docker build -f Dockerfile.sidecar -t dory-sidecar:1.0.0 .
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[kubernetes]
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: 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**
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
- - [Developer Guide](docs/DEVELOPER_GUIDE.md) - Advanced topics
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 `minimal-processor-py` as a template for new processors.
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=n7CwUZzXdXvgax6kcNz3GHpOC5bCZab40PWkoLvkep0,1599
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=Ydh-2pEZbiUPpNfSrNviGPooYsZfirnKbF1iyYP-MJ8,1136
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=Ou4wythmd69t4XXZmooIHLDonx0xTVF7fMySfcJ1A2Y,4586
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=17vfNVRmkAcKKNF_LbOJOiMTzpe6pBAhOCf4D_konbk,10525
29
- dory/k8s/__init__.py,sha256=LpIiEV3yPmi9yeUsC0HAmWiFw0ih1XvN8NlAZb_Z0iU,259
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=cUdZG-aWQj40ZaPqG7-ooREivFOrFrEbyvGbc_IlnRo,313
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=XqbLPbuRnMkOy3hXF_30yt9oqyKKM8nlneAoOnz9J0g,10710
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.0.dist-info/METADATA,sha256=yTsEqHrg3PwI3VswHrPnFLDXyYZ4zkeXo6sYqK8jdzo,16699
66
- dory_sdk-2.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
67
- dory_sdk-2.1.0.dist-info/entry_points.txt,sha256=9r2K0MclUnqanWdzbR-jlyXVNJYuE8ZvwuADiozi3Rw,89
68
- dory_sdk-2.1.0.dist-info/top_level.txt,sha256=lbgLxVkyGEP63BrBq53S9m0Mv7t-ZP-B0WHKTmOpOz0,5
69
- dory_sdk-2.1.0.dist-info/RECORD,,
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,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dory = dory.cli.main:main
dory/sidecar/__init__.py DELETED
@@ -1,6 +0,0 @@
1
- """Dory Sidecar - Lightweight health proxy for non-SDK apps."""
2
-
3
- from dory.sidecar.server import SidecarServer
4
- from dory.sidecar.main import run_sidecar
5
-
6
- __all__ = ["SidecarServer", "run_sidecar"]
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()
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- dory = dory.cli.main:main
3
- dory-sidecar = dory.sidecar.main:run_sidecar