flowstash-runtime 0.9.2__tar.gz → 0.10.0__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.
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/PKG-INFO +5 -3
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/pyproject.toml +4 -3
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_backend.py +12 -2
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/http_entrypoint.py +1 -1
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/lease_client.py +18 -4
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/managed_consumer.py +3 -3
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/app.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/router.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/runtime.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/bootstrap.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/consumer_middleware.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_consumer.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/entrypoint.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/README.md +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/__init__.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/drain.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/feed_consumer.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/main.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/task_resolver.py +0 -0
- {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/runner.py +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flowstash-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Actual runtime engine and bootstrap layer for the flowstash platform.
|
|
5
|
+
License: MIT
|
|
5
6
|
Author: juraj.bezdek@gmail.com
|
|
6
7
|
Author-email: juraj.bezdek@gmail.com
|
|
7
8
|
Requires-Python: >=3.11
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
10
|
Classifier: Programming Language :: Python :: 3
|
|
9
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -12,8 +14,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
12
14
|
Requires-Dist: apscheduler (>=3.11.0)
|
|
13
15
|
Requires-Dist: dramatiq (>=1.16.0)
|
|
14
16
|
Requires-Dist: fastapi (>=0.110.0)
|
|
15
|
-
Requires-Dist: flowstash-clients (>=0.
|
|
16
|
-
Requires-Dist: flowstash-lib (>=0.
|
|
17
|
+
Requires-Dist: flowstash-clients (>=0.10.0,<0.11.0)
|
|
18
|
+
Requires-Dist: flowstash-lib (>=0.10.0,<0.11.0)
|
|
17
19
|
Requires-Dist: pyyaml (>=6.0.1)
|
|
18
20
|
Requires-Dist: uvicorn (>=0.29.0)
|
|
19
21
|
Requires-Dist: websockets (>=14.0)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flowstash-runtime"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Actual runtime engine and bootstrap layer for the flowstash platform."
|
|
5
5
|
authors = [{name = "juraj.bezdek@gmail.com", email = "juraj.bezdek@gmail.com"}]
|
|
6
|
+
license = "MIT"
|
|
6
7
|
requires-python = ">=3.11"
|
|
7
8
|
dependencies = [
|
|
8
|
-
"flowstash-clients>=0.
|
|
9
|
-
"flowstash-lib>=0.
|
|
9
|
+
"flowstash-clients>=0.10.0,<0.11.0",
|
|
10
|
+
"flowstash-lib>=0.10.0,<0.11.0",
|
|
10
11
|
"fastapi>=0.110.0",
|
|
11
12
|
"uvicorn>=0.29.0",
|
|
12
13
|
"dramatiq>=1.16.0",
|
|
@@ -8,6 +8,7 @@ import dramatiq
|
|
|
8
8
|
import logging
|
|
9
9
|
import asyncio
|
|
10
10
|
import threading
|
|
11
|
+
from datetime import datetime, UTC
|
|
11
12
|
from typing import Any, Callable, Optional, Mapping, List, Union
|
|
12
13
|
from flowstash.queue.backend import JobHandle, TaskBackend, Schedule
|
|
13
14
|
from flowstash.context import IntegrationContext, integration_context, current_context
|
|
@@ -244,8 +245,17 @@ class DramatiqBackend(TaskBackend):
|
|
|
244
245
|
if pipeline:
|
|
245
246
|
headers["fw.pipeline"] = pipeline
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
# Framework API expresses numeric delays in seconds; dramatiq wants ms.
|
|
249
|
+
delay: Optional[int] = None
|
|
250
|
+
if isinstance(eta_or_delay, (int, float)):
|
|
251
|
+
delay = max(0, int(eta_or_delay * 1000))
|
|
252
|
+
elif isinstance(eta_or_delay, datetime):
|
|
253
|
+
eta = (
|
|
254
|
+
eta_or_delay
|
|
255
|
+
if eta_or_delay.tzinfo
|
|
256
|
+
else eta_or_delay.replace(tzinfo=UTC)
|
|
257
|
+
)
|
|
258
|
+
delay = max(0, int((eta - datetime.now(UTC)).total_seconds() * 1000))
|
|
249
259
|
|
|
250
260
|
message = func.send_with_options(
|
|
251
261
|
args=args, kwargs=kwargs, delay=delay, headers=headers
|
|
@@ -170,7 +170,7 @@ def _get_drain_controller(request: Request) -> ManagedTaskDrainController:
|
|
|
170
170
|
return controller
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
async def _flush_observability(timeout_s: float =
|
|
173
|
+
async def _flush_observability(timeout_s: float = 30.0) -> None:
|
|
174
174
|
import asyncio
|
|
175
175
|
from flowstash.observability.ingestion import AsyncManager
|
|
176
176
|
|
|
@@ -20,6 +20,8 @@ import asyncio
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
22
|
import os
|
|
23
|
+
import random
|
|
24
|
+
import uuid
|
|
23
25
|
from dataclasses import dataclass
|
|
24
26
|
from typing import Dict, List, Optional, Set
|
|
25
27
|
|
|
@@ -83,6 +85,9 @@ class LeaseBrokerClient:
|
|
|
83
85
|
self._acquire_timeout = acquire_timeout
|
|
84
86
|
self._reconnect_min = reconnect_min
|
|
85
87
|
self._reconnect_max = reconnect_max
|
|
88
|
+
# Stable per-process identity so a reconnecting worker reclaims (transfers)
|
|
89
|
+
# its own leases instead of conflicting with its not-yet-reaped old conn.
|
|
90
|
+
self._worker_id = os.getenv("CLOUD_RUN_EXECUTION") or uuid.uuid4().hex
|
|
86
91
|
|
|
87
92
|
self._held: Set[str] = set()
|
|
88
93
|
self._waiters: Dict[str, List[asyncio.Future]] = {}
|
|
@@ -110,13 +115,17 @@ class LeaseBrokerClient:
|
|
|
110
115
|
except Exception:
|
|
111
116
|
pass
|
|
112
117
|
|
|
118
|
+
def _connect_url(self) -> str:
|
|
119
|
+
sep = "&" if "?" in self._url else "?"
|
|
120
|
+
return f"{self._url}{sep}worker_id={self._worker_id}"
|
|
121
|
+
|
|
113
122
|
async def _run(self) -> None:
|
|
114
123
|
backoff = self._reconnect_min
|
|
115
124
|
headers = {"Authorization": f"Bearer {self._token}"}
|
|
116
125
|
while not self._closing:
|
|
117
126
|
try:
|
|
118
127
|
async with websockets.connect(
|
|
119
|
-
self.
|
|
128
|
+
self._connect_url(), additional_headers=headers, open_timeout=10
|
|
120
129
|
) as ws:
|
|
121
130
|
self._ws = ws
|
|
122
131
|
backoff = self._reconnect_min
|
|
@@ -138,7 +147,9 @@ class LeaseBrokerClient:
|
|
|
138
147
|
self._ws = None
|
|
139
148
|
if self._closing:
|
|
140
149
|
break
|
|
141
|
-
|
|
150
|
+
# Jittered backoff so a broker restart doesn't trigger a synchronized
|
|
151
|
+
# reconnect storm from every worker at once.
|
|
152
|
+
await asyncio.sleep(backoff * (0.5 + 0.5 * random.random()))
|
|
142
153
|
backoff = min(backoff * 2, self._reconnect_max)
|
|
143
154
|
|
|
144
155
|
# ── messaging ──────────────────────────────────────────────────────────
|
|
@@ -250,8 +261,11 @@ def get_lease_client() -> Optional[LeaseBrokerClient]:
|
|
|
250
261
|
return _client
|
|
251
262
|
_resolved = True
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
|
|
264
|
+
# Default OFF: deploying the worker code is inert until ops explicitly enables
|
|
265
|
+
# the guard (after the broker is deployed and validated). Otherwise an
|
|
266
|
+
# unreachable broker would fail every task closed (503).
|
|
267
|
+
if os.getenv("LEASE_BROKER_ENABLED", "false").lower() not in ("1", "true", "yes"):
|
|
268
|
+
logger.info("lease broker disabled (set LEASE_BROKER_ENABLED=true to enable)")
|
|
255
269
|
return None
|
|
256
270
|
if websockets is None:
|
|
257
271
|
logger.warning("lease broker disabled: 'websockets' not installed")
|
|
@@ -411,7 +411,7 @@ class ManagedConsumer(TaskConsumer):
|
|
|
411
411
|
except ValueError as e:
|
|
412
412
|
logger.error(f"[run-task] {e}")
|
|
413
413
|
await _record_missing_task(task_name)
|
|
414
|
-
await asyncio.to_thread(AsyncManager.get_instance().flush,
|
|
414
|
+
await asyncio.to_thread(AsyncManager.get_instance().flush, 30.0)
|
|
415
415
|
sys.exit(1)
|
|
416
416
|
|
|
417
417
|
# Idempotency guard via the lease broker. Unlike HTTP tasks, a job that
|
|
@@ -428,7 +428,7 @@ class ManagedConsumer(TaskConsumer):
|
|
|
428
428
|
run_id,
|
|
429
429
|
res.outcome,
|
|
430
430
|
)
|
|
431
|
-
await asyncio.to_thread(AsyncManager.get_instance().flush,
|
|
431
|
+
await asyncio.to_thread(AsyncManager.get_instance().flush, 30.0)
|
|
432
432
|
sys.exit(0)
|
|
433
433
|
elif res.outcome == ACQUIRED:
|
|
434
434
|
lease_held = True
|
|
@@ -450,7 +450,7 @@ class ManagedConsumer(TaskConsumer):
|
|
|
450
450
|
if lease_held:
|
|
451
451
|
await lease.release(run_id, SUCCEEDED if success else FAILED)
|
|
452
452
|
|
|
453
|
-
await asyncio.to_thread(AsyncManager.get_instance().flush,
|
|
453
|
+
await asyncio.to_thread(AsyncManager.get_instance().flush, 30.0)
|
|
454
454
|
sys.exit(0 if success else 1)
|
|
455
455
|
|
|
456
456
|
async def stop(self) -> None:
|
|
File without changes
|
{flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/router.py
RENAMED
|
File without changes
|
{flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/__init__.py
RENAMED
|
File without changes
|
{flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/runtime.py
RENAMED
|
File without changes
|
{flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|