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.
Files changed (26) hide show
  1. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/PKG-INFO +5 -3
  2. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/pyproject.toml +4 -3
  3. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_backend.py +12 -2
  4. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/http_entrypoint.py +1 -1
  5. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/lease_client.py +18 -4
  6. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/managed_consumer.py +3 -3
  7. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/__init__.py +0 -0
  8. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/__init__.py +0 -0
  9. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/app.py +0 -0
  10. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/ingress/router.py +0 -0
  11. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/__init__.py +0 -0
  12. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/wiring/runtime.py +0 -0
  13. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/__init__.py +0 -0
  14. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/__init__.py +0 -0
  15. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/__init__.py +0 -0
  16. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/bootstrap.py +0 -0
  17. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/consumer_middleware.py +0 -0
  18. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_consumer.py +0 -0
  19. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/dramatiq/entrypoint.py +0 -0
  20. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/README.md +0 -0
  21. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/__init__.py +0 -0
  22. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/drain.py +0 -0
  23. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/feed_consumer.py +0 -0
  24. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/main.py +0 -0
  25. {flowstash_runtime-0.9.2 → flowstash_runtime-0.10.0}/src/flowstash/runtime/worker/backends/managed/task_resolver.py +0 -0
  26. {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.9.2
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.9.2,<0.10.0)
16
- Requires-Dist: flowstash-lib (>=0.9.2,<0.10.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.9.2"
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.2,<0.10.0",
9
- "flowstash-lib>=0.9.2,<0.10.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
- delay = eta_or_delay if isinstance(eta_or_delay, (int, float)) else None
248
- # Handle datetime eta if needed...
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 = 15.0) -> None:
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._url, additional_headers=headers, open_timeout=10
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
- await asyncio.sleep(backoff)
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
- if os.getenv("LEASE_BROKER_ENABLED", "true").lower() in ("0", "false", "no"):
254
- logger.info("lease broker disabled via LEASE_BROKER_ENABLED")
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, 15.0)
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, 15.0)
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, 15.0)
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: