-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworker_ack.py
More file actions
108 lines (91 loc) · 3.67 KB
/
Copy pathworker_ack.py
File metadata and controls
108 lines (91 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""JetStream ack heartbeats and durable subscribe helpers for GPU workers.
Durable ack_wait / max_deliver are provisioned from
``ct-platform/backend/config/nats_streams.yaml`` via
``coolify-provisioning/scripts/ensure-jetstream-streams.sh``.
Workers subscribe only; they do not mutate durables on the broker.
"""
from __future__ import annotations
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator
from nats.js.api import AckPolicy, ConsumerConfig, DeliverPolicy
logger = logging.getLogger(__name__)
# Defaults match nats_streams.yaml pdf_service durables (env override for test only).
DEFAULT_ACK_WAIT_S = int(os.getenv("NATS_ACK_WAIT_S", "900"))
DEFAULT_MAX_DELIVER = int(os.getenv("NATS_MAX_DELIVER", "5"))
DEFAULT_HEARTBEAT_S = int(os.getenv("NATS_ACK_HEARTBEAT_S", "60"))
def durable_consumer_config(durable: str, *, filter_subject: str) -> ConsumerConfig:
"""Pull-consumer config: long ack window, finite redelivery (create path only)."""
return ConsumerConfig(
durable_name=durable,
ack_policy=AckPolicy.EXPLICIT,
deliver_policy=DeliverPolicy.ALL,
filter_subject=filter_subject,
ack_wait=DEFAULT_ACK_WAIT_S,
max_deliver=DEFAULT_MAX_DELIVER,
max_ack_pending=1,
)
async def ensure_pull_subscribe(
js: Any,
*,
subject: str,
durable: str,
stream: str,
) -> Any:
"""Pull-subscribe; warn if durable safety knobs drift (fix via ensure script)."""
config = durable_consumer_config(durable, filter_subject=subject)
try:
info = await js.consumer_info(stream, durable)
have_ack = int(info.config.ack_wait or 0)
have_max = int(info.config.max_deliver or 0)
if have_ack != DEFAULT_ACK_WAIT_S or have_max != DEFAULT_MAX_DELIVER:
logger.error(
"consumer %s/%s drift: ack_wait=%s (want %s) max_deliver=%s (want %s); "
"run coolify-provisioning/scripts/ensure-jetstream-streams.sh --via-gpu",
stream,
durable,
have_ack,
DEFAULT_ACK_WAIT_S,
have_max,
DEFAULT_MAX_DELIVER,
)
except (OSError, RuntimeError, ValueError, KeyError, TypeError) as exc:
# Durable may not exist yet — pull_subscribe creates it with config.
if "not found" not in str(exc).lower() and "10014" not in str(exc):
logger.warning("consumer_info %s: %s", durable, exc)
return await js.pull_subscribe(
subject=subject,
durable=durable,
stream=stream,
config=config,
)
@asynccontextmanager
async def ack_heartbeat(message: Any, *, interval_s: float | None = None) -> AsyncIterator[None]:
"""Extend JetStream ack deadline while a long job runs."""
period = float(interval_s if interval_s is not None else DEFAULT_HEARTBEAT_S)
async def _beat() -> None:
while True:
await asyncio.sleep(period)
try:
await message.in_progress()
except (OSError, RuntimeError, ValueError, TypeError) as exc:
logger.warning("in_progress heartbeat failed: %s", exc)
return
task = asyncio.create_task(_beat())
try:
yield
finally:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
def is_retryable_error(exc: BaseException) -> bool:
"""Transient IO / broker errors may NAK; application errors should TERM."""
if isinstance(exc, MemoryError):
return False
if isinstance(exc, (OSError, TimeoutError, asyncio.TimeoutError, ConnectionError)):
return True
return False