vercel-queue 0.7.0__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.
- vercel/queue/__init__.py +8 -0
- vercel/queue/__main__.py +10 -0
- vercel/queue/_internal/__init__.py +1 -0
- vercel/queue/_internal/api_async.py +219 -0
- vercel/queue/_internal/api_common.py +104 -0
- vercel/queue/_internal/api_sync.py +137 -0
- vercel/queue/_internal/asgi.py +145 -0
- vercel/queue/_internal/asynctools.py +40 -0
- vercel/queue/_internal/cli.py +313 -0
- vercel/queue/_internal/client.py +1109 -0
- vercel/queue/_internal/client_sync.py +432 -0
- vercel/queue/_internal/config.py +160 -0
- vercel/queue/_internal/constants.py +50 -0
- vercel/queue/_internal/devserver.py +205 -0
- vercel/queue/_internal/embedded.py +1900 -0
- vercel/queue/_internal/errors.py +262 -0
- vercel/queue/_internal/http.py +634 -0
- vercel/queue/_internal/lease.py +1007 -0
- vercel/queue/_internal/log.py +143 -0
- vercel/queue/_internal/messages.py +122 -0
- vercel/queue/_internal/multipart.py +255 -0
- vercel/queue/_internal/names.py +101 -0
- vercel/queue/_internal/polling.py +200 -0
- vercel/queue/_internal/push.py +246 -0
- vercel/queue/_internal/response.py +111 -0
- vercel/queue/_internal/retry.py +86 -0
- vercel/queue/_internal/streams.py +313 -0
- vercel/queue/_internal/subscribers.py +1025 -0
- vercel/queue/_internal/transports.py +363 -0
- vercel/queue/_internal/types.py +300 -0
- vercel/queue/_internal/typeutils.py +203 -0
- vercel/queue/devserver.py +24 -0
- vercel/queue/embedded.py +50 -0
- vercel/queue/py.typed +1 -0
- vercel/queue/sync.py +8 -0
- vercel/queue/testing/__init__.py +14 -0
- vercel/queue/testing/pytest.py +42 -0
- vercel/queue/testing/state.py +32 -0
- vercel/queue/version.py +3 -0
- vercel_queue-0.7.0.dist-info/METADATA +681 -0
- vercel_queue-0.7.0.dist-info/RECORD +43 -0
- vercel_queue-0.7.0.dist-info/WHEEL +4 -0
- vercel_queue-0.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"""Lease lifecycle helpers for queue message processing.
|
|
2
|
+
|
|
3
|
+
Automatic lease renewal is coordinated through one process-wide daemon thread
|
|
4
|
+
running an AnyIO worker. Public sync and async clients enqueue renewal commands
|
|
5
|
+
into an AnyIO memory stream owned by that worker, while the worker runs one
|
|
6
|
+
AnyIO task per active renewal.
|
|
7
|
+
|
|
8
|
+
Registration waits until the worker records the renewal and schedules its task,
|
|
9
|
+
but it does not wait for the first lease extension to complete. This surfaces
|
|
10
|
+
delivery failures at handler startup without coupling startup to renewal
|
|
11
|
+
network I/O. Fire-and-forget stop paths are still logged asynchronously.
|
|
12
|
+
|
|
13
|
+
Stopping a renewal removes future work and cancels any in-flight extension. The
|
|
14
|
+
RetryAfter path waits for that cancellation so a late automatic renewal cannot
|
|
15
|
+
overwrite the handler's explicit visibility update, while acknowledge paths can
|
|
16
|
+
avoid that tail latency because late renewals are harmless after ACK.
|
|
17
|
+
|
|
18
|
+
Lease renewal invariants:
|
|
19
|
+
|
|
20
|
+
- A renewal handle is single-use and idempotently stoppable.
|
|
21
|
+
- Start waits for worker scheduling, not lease extension I/O.
|
|
22
|
+
- Stop with wait=True waits for in-flight renewal cancellation or completion.
|
|
23
|
+
- At most one renewal for a token may be in flight at a time.
|
|
24
|
+
- Command submission uses a large bounded AnyIO memory stream.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any, NewType, Protocol, TypeAlias
|
|
30
|
+
from typing_extensions import Self
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import math
|
|
34
|
+
import threading
|
|
35
|
+
import time
|
|
36
|
+
from collections import OrderedDict
|
|
37
|
+
from collections.abc import Callable, Coroutine, Iterator, MutableMapping
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from datetime import datetime, timezone
|
|
40
|
+
from types import TracebackType
|
|
41
|
+
|
|
42
|
+
import anyio
|
|
43
|
+
import anyio.from_thread
|
|
44
|
+
from anyio import to_thread
|
|
45
|
+
from anyio.abc import ObjectReceiveStream, ObjectSendStream, TaskGroup
|
|
46
|
+
from anyio.lowlevel import EventLoopToken, current_token
|
|
47
|
+
from anyio.streams.memory import MemoryObjectSendStream
|
|
48
|
+
|
|
49
|
+
from vercel.oidc import get_vercel_oidc_token_sync
|
|
50
|
+
|
|
51
|
+
from .asynctools import iter_coroutine
|
|
52
|
+
from .errors import MessageNotFoundError, QueueError, ThrottledError
|
|
53
|
+
from .log import debug_log
|
|
54
|
+
from .retry import retry_async_follow_up, retry_sync_follow_up
|
|
55
|
+
from .streams import (
|
|
56
|
+
AsyncStreamPayload,
|
|
57
|
+
AsyncTextStreamPayload,
|
|
58
|
+
SyncStreamPayload,
|
|
59
|
+
SyncTextStreamPayload,
|
|
60
|
+
)
|
|
61
|
+
from .types import (
|
|
62
|
+
Duration,
|
|
63
|
+
Message,
|
|
64
|
+
MessageMetadata,
|
|
65
|
+
duration_to_float_seconds,
|
|
66
|
+
duration_to_seconds,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AsyncExtendMessageLease(Protocol):
|
|
71
|
+
def __call__(
|
|
72
|
+
self,
|
|
73
|
+
message: Message[Any] | MessageMetadata,
|
|
74
|
+
duration: Duration,
|
|
75
|
+
) -> Coroutine[Any, Any, None]: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ExtendMessageLease(Protocol):
|
|
79
|
+
def __call__(self, message: Message[Any] | MessageMetadata, duration: Duration) -> None: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _LeaseStopEvent(Protocol):
|
|
83
|
+
def set(self) -> None: ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
DEFAULT_PROCESSING_LEASE_SECONDS = 300
|
|
87
|
+
MIN_PROCESSING_LEASE_SECONDS = 30
|
|
88
|
+
MAX_VISIBILITY_TIMEOUT_SECONDS = 3600
|
|
89
|
+
_TRANSIENT_LEASE_RETRY_SECONDS = 3.0
|
|
90
|
+
_DUE_RENEWAL_START_GRACE_SECONDS = 0.01
|
|
91
|
+
_IMMEDIATE_RENEWAL_GRACE_SECONDS = 10.0
|
|
92
|
+
_STOPPED_LEASE_TOKEN_CACHE_SIZE = 1024
|
|
93
|
+
_LEASE_STOP_WAIT_TIMEOUT_SECONDS = 5.0
|
|
94
|
+
_LEASE_START_WAIT_TIMEOUT_SECONDS = 5.0
|
|
95
|
+
_LEASE_COMMAND_BUFFER_SIZE = 65_536
|
|
96
|
+
_LeaseRenewalToken = NewType("_LeaseRenewalToken", object)
|
|
97
|
+
logger = logging.getLogger(__name__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LeaseAsyncClient(Protocol):
|
|
101
|
+
def _renew_lease(
|
|
102
|
+
self,
|
|
103
|
+
message: Message[Any],
|
|
104
|
+
duration: Duration,
|
|
105
|
+
) -> Coroutine[Any, Any, None]: ...
|
|
106
|
+
|
|
107
|
+
def _extend_lease(
|
|
108
|
+
self,
|
|
109
|
+
message: Message[Any] | MessageMetadata,
|
|
110
|
+
duration: Duration,
|
|
111
|
+
) -> Coroutine[Any, Any, None]: ...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(kw_only=True)
|
|
115
|
+
class _LeaseExtensionRequest:
|
|
116
|
+
token: _LeaseRenewalToken
|
|
117
|
+
message: Message[Any]
|
|
118
|
+
client: LeaseAsyncClient
|
|
119
|
+
lease_seconds: int
|
|
120
|
+
next_extension_at: float
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(kw_only=True)
|
|
124
|
+
class _LeaseExtensionStart:
|
|
125
|
+
request: _LeaseExtensionRequest
|
|
126
|
+
scheduled_event: threading.Event | anyio.Event
|
|
127
|
+
event_loop_token: EventLoopToken | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, kw_only=True)
|
|
131
|
+
class _LeaseExtensionStop:
|
|
132
|
+
token: _LeaseRenewalToken
|
|
133
|
+
done_send: ObjectSendStream[None] | None = None
|
|
134
|
+
done_event: _LeaseStopEvent | None = None
|
|
135
|
+
event_loop_token: EventLoopToken | None = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class _LeaseWorkerShutdown:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_LeaseExtensionCommand: TypeAlias = (
|
|
144
|
+
_LeaseExtensionStart | _LeaseExtensionStop | _LeaseWorkerShutdown
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(kw_only=True)
|
|
149
|
+
class _LeaseRenewalTask:
|
|
150
|
+
request: _LeaseExtensionRequest
|
|
151
|
+
cancel_scope: anyio.CancelScope
|
|
152
|
+
stop_waiters: list[_LeaseExtensionStop]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(kw_only=True)
|
|
156
|
+
class _LeaseWorkerRuntimeState:
|
|
157
|
+
active: dict[_LeaseRenewalToken, _LeaseRenewalTask]
|
|
158
|
+
stopped: OrderedDict[_LeaseRenewalToken, None]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(kw_only=True)
|
|
162
|
+
class _LeaseWorkerState:
|
|
163
|
+
command_send: MemoryObjectSendStream[_LeaseExtensionCommand] | None = None
|
|
164
|
+
worker_token: EventLoopToken | None = None
|
|
165
|
+
started: bool = False
|
|
166
|
+
thread: threading.Thread | None = None
|
|
167
|
+
startup_error: BaseException | None = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
_lease_worker_state = _LeaseWorkerState()
|
|
171
|
+
_lease_worker_ready = threading.Event()
|
|
172
|
+
_lease_worker_lock = threading.Lock()
|
|
173
|
+
_LEASE_WORKER_START_TIMEOUT_SECONDS = 5.0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def visibility_timeout_seconds(duration: Duration, *, name: str = "duration") -> int:
|
|
177
|
+
exact_seconds = duration_to_float_seconds(duration)
|
|
178
|
+
if not math.isfinite(exact_seconds):
|
|
179
|
+
raise ValueError(f"{name} must be finite")
|
|
180
|
+
if exact_seconds < 0:
|
|
181
|
+
raise ValueError(f"{name} must be non-negative")
|
|
182
|
+
if exact_seconds > MAX_VISIBILITY_TIMEOUT_SECONDS:
|
|
183
|
+
raise ValueError(f"{name} cannot exceed {MAX_VISIBILITY_TIMEOUT_SECONDS} seconds")
|
|
184
|
+
seconds = duration_to_seconds(duration)
|
|
185
|
+
if exact_seconds > 0 and seconds == 0:
|
|
186
|
+
raise ValueError(f"{name} must be at least 1 second or exactly 0")
|
|
187
|
+
return seconds
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def processing_lease_seconds(duration: Duration | None, *, name: str = "lease_duration") -> int:
|
|
191
|
+
if duration is None:
|
|
192
|
+
return DEFAULT_PROCESSING_LEASE_SECONDS
|
|
193
|
+
seconds = visibility_timeout_seconds(duration, name=name)
|
|
194
|
+
if seconds == 0:
|
|
195
|
+
raise ValueError(f"{name} must be positive for automatic renewal")
|
|
196
|
+
return max(MIN_PROCESSING_LEASE_SECONDS, seconds)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _next_extension_delay(metadata: MessageMetadata, lease_seconds: int) -> float:
|
|
200
|
+
if metadata.visibility_deadline is not None:
|
|
201
|
+
remaining = (metadata.visibility_deadline - datetime.now(timezone.utc)).total_seconds()
|
|
202
|
+
if remaining <= _IMMEDIATE_RENEWAL_GRACE_SECONDS:
|
|
203
|
+
return 0.0
|
|
204
|
+
return max(0.0, remaining - _IMMEDIATE_RENEWAL_GRACE_SECONDS)
|
|
205
|
+
return _renewal_interval_seconds(lease_seconds)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _renewal_interval_seconds(visibility_timeout_seconds: float) -> float:
|
|
209
|
+
return min(60.0, max(10.0, visibility_timeout_seconds / 5))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _next_extension_at(metadata: MessageMetadata, lease_seconds: int) -> float:
|
|
213
|
+
delay = _next_extension_delay(metadata, lease_seconds)
|
|
214
|
+
if delay <= 0.0:
|
|
215
|
+
delay = _DUE_RENEWAL_START_GRACE_SECONDS
|
|
216
|
+
return time.monotonic() + delay
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LeaseRenewal:
|
|
220
|
+
"""Single-use context manager for keeping a message lease alive."""
|
|
221
|
+
|
|
222
|
+
def __init__(
|
|
223
|
+
self,
|
|
224
|
+
message: Message[Any],
|
|
225
|
+
client: LeaseAsyncClient,
|
|
226
|
+
lease_duration: Duration | None = None,
|
|
227
|
+
) -> None:
|
|
228
|
+
self._message = message
|
|
229
|
+
self._client = client
|
|
230
|
+
self._lease_seconds = processing_lease_seconds(lease_duration)
|
|
231
|
+
self._token: _LeaseRenewalToken | None = None
|
|
232
|
+
self._entered = False
|
|
233
|
+
self._closed = False
|
|
234
|
+
self._closing = False
|
|
235
|
+
|
|
236
|
+
def __enter__(self) -> Self:
|
|
237
|
+
self.start()
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def start(self) -> None:
|
|
241
|
+
"""Start renewing the lease."""
|
|
242
|
+
self._start_with_wait()
|
|
243
|
+
|
|
244
|
+
async def start_async(self) -> None:
|
|
245
|
+
"""Start renewing the lease from async code."""
|
|
246
|
+
await self._start_with_wait_async()
|
|
247
|
+
|
|
248
|
+
def _start_with_wait(self) -> None:
|
|
249
|
+
request = self._prepare_start()
|
|
250
|
+
if request is None:
|
|
251
|
+
return
|
|
252
|
+
try:
|
|
253
|
+
_send_lease_extension_start(request)
|
|
254
|
+
except Exception:
|
|
255
|
+
self._reset_start_after_failure()
|
|
256
|
+
raise
|
|
257
|
+
self._log_start_result()
|
|
258
|
+
|
|
259
|
+
async def _start_with_wait_async(self) -> None:
|
|
260
|
+
request = self._prepare_start()
|
|
261
|
+
if request is None:
|
|
262
|
+
return
|
|
263
|
+
try:
|
|
264
|
+
await _send_lease_extension_start_async(request)
|
|
265
|
+
except BaseException:
|
|
266
|
+
await _send_best_effort_async_start_failure_stop(request.token)
|
|
267
|
+
self._reset_start_after_failure()
|
|
268
|
+
raise
|
|
269
|
+
self._log_start_result()
|
|
270
|
+
|
|
271
|
+
def _prepare_start(self) -> _LeaseExtensionRequest | None:
|
|
272
|
+
if self._entered:
|
|
273
|
+
raise RuntimeError("lease renewal handles are single-use")
|
|
274
|
+
if self._closed:
|
|
275
|
+
raise RuntimeError("lease renewal handle is closed")
|
|
276
|
+
if self._message.metadata.receipt_handle is None:
|
|
277
|
+
return None
|
|
278
|
+
self._entered = True
|
|
279
|
+
_prime_oidc_token_cache()
|
|
280
|
+
_ensure_lease_renewal_thread()
|
|
281
|
+
self._token = _LeaseRenewalToken(object())
|
|
282
|
+
return _LeaseExtensionRequest(
|
|
283
|
+
token=self._token,
|
|
284
|
+
message=self._message,
|
|
285
|
+
client=self._client,
|
|
286
|
+
lease_seconds=self._lease_seconds,
|
|
287
|
+
next_extension_at=_next_extension_at(self._message.metadata, self._lease_seconds),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _reset_start_after_failure(self) -> None:
|
|
291
|
+
self._token = None
|
|
292
|
+
self._entered = False
|
|
293
|
+
|
|
294
|
+
def _log_start_result(self) -> None:
|
|
295
|
+
debug_log(
|
|
296
|
+
"lease.renewal_registered",
|
|
297
|
+
message_id=self._message.metadata.message_id,
|
|
298
|
+
topic=self._message.metadata.topic,
|
|
299
|
+
consumer_group=self._message.metadata.consumer_group,
|
|
300
|
+
lease_seconds=self._lease_seconds,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def __exit__(
|
|
304
|
+
self,
|
|
305
|
+
exc_type: type[BaseException] | None,
|
|
306
|
+
exc: BaseException | None,
|
|
307
|
+
traceback: TracebackType | None,
|
|
308
|
+
) -> None:
|
|
309
|
+
self.stop()
|
|
310
|
+
|
|
311
|
+
def stop(self, *, wait: bool = True) -> None:
|
|
312
|
+
"""Stop renewing the lease. Safe to call more than once."""
|
|
313
|
+
if self._closed or self._closing:
|
|
314
|
+
return
|
|
315
|
+
token = self._token
|
|
316
|
+
if token is None:
|
|
317
|
+
self._closed = True
|
|
318
|
+
return
|
|
319
|
+
self._closing = True
|
|
320
|
+
try:
|
|
321
|
+
stopped = self._send_stop(token, wait=wait)
|
|
322
|
+
except Exception:
|
|
323
|
+
self._closing = False
|
|
324
|
+
raise
|
|
325
|
+
self._token = None
|
|
326
|
+
self._closed = True
|
|
327
|
+
self._closing = False
|
|
328
|
+
self._log_stop_result(stopped=stopped)
|
|
329
|
+
|
|
330
|
+
async def stop_async(self, *, wait: bool = True) -> None:
|
|
331
|
+
"""Stop renewing the lease without blocking the current event loop."""
|
|
332
|
+
if self._closed or self._closing:
|
|
333
|
+
return
|
|
334
|
+
token = self._token
|
|
335
|
+
if token is None:
|
|
336
|
+
self._closed = True
|
|
337
|
+
return
|
|
338
|
+
self._closing = True
|
|
339
|
+
try:
|
|
340
|
+
stopped = await self._send_stop_async(token, wait=wait)
|
|
341
|
+
except BaseException:
|
|
342
|
+
self._closing = False
|
|
343
|
+
raise
|
|
344
|
+
self._token = None
|
|
345
|
+
self._closed = True
|
|
346
|
+
self._closing = False
|
|
347
|
+
self._log_stop_result(stopped=stopped)
|
|
348
|
+
|
|
349
|
+
def _send_stop(self, token: _LeaseRenewalToken, *, wait: bool) -> bool:
|
|
350
|
+
if wait:
|
|
351
|
+
return _send_lease_extension_stop(token)
|
|
352
|
+
self._send_best_effort_stop(token)
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
async def _send_stop_async(self, token: _LeaseRenewalToken, *, wait: bool) -> bool:
|
|
356
|
+
if wait:
|
|
357
|
+
return await _send_lease_extension_stop_async(token)
|
|
358
|
+
await self._send_best_effort_stop_async(token)
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
def _log_stop_result(self, *, stopped: bool) -> None:
|
|
362
|
+
if stopped:
|
|
363
|
+
debug_log(
|
|
364
|
+
"lease.renewal_stopped",
|
|
365
|
+
message_id=self._message.metadata.message_id,
|
|
366
|
+
topic=self._message.metadata.topic,
|
|
367
|
+
consumer_group=self._message.metadata.consumer_group,
|
|
368
|
+
)
|
|
369
|
+
return
|
|
370
|
+
logger.warning(
|
|
371
|
+
"timed out waiting for queue lease renewal to stop for message %s on topic %s/%s",
|
|
372
|
+
self._message.metadata.message_id,
|
|
373
|
+
self._message.metadata.topic,
|
|
374
|
+
self._message.metadata.consumer_group,
|
|
375
|
+
)
|
|
376
|
+
debug_log(
|
|
377
|
+
"lease.renewal_stop_timeout",
|
|
378
|
+
message_id=self._message.metadata.message_id,
|
|
379
|
+
topic=self._message.metadata.topic,
|
|
380
|
+
consumer_group=self._message.metadata.consumer_group,
|
|
381
|
+
timeout_seconds=_LEASE_STOP_WAIT_TIMEOUT_SECONDS,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _send_best_effort_stop(self, token: _LeaseRenewalToken) -> None:
|
|
385
|
+
try:
|
|
386
|
+
_send_lease_command(_LeaseExtensionStop(token=token))
|
|
387
|
+
except Exception as exc: # noqa: BLE001
|
|
388
|
+
_log_best_effort_lease_stop_failure(exc)
|
|
389
|
+
|
|
390
|
+
async def _send_best_effort_stop_async(self, token: _LeaseRenewalToken) -> None:
|
|
391
|
+
try:
|
|
392
|
+
await to_thread.run_sync(_send_lease_command, _LeaseExtensionStop(token=token))
|
|
393
|
+
except Exception as exc: # noqa: BLE001
|
|
394
|
+
_log_best_effort_lease_stop_failure(exc)
|
|
395
|
+
|
|
396
|
+
async def extend_async(self, duration: Duration) -> None:
|
|
397
|
+
"""Extend the lease immediately if this message has lease metadata.
|
|
398
|
+
|
|
399
|
+
Applies settlement retry semantics: transient failures are retried
|
|
400
|
+
and an already-missing lease is tolerated with a warning.
|
|
401
|
+
"""
|
|
402
|
+
if self._message.metadata.receipt_handle is None:
|
|
403
|
+
return
|
|
404
|
+
await retry_message_after_async(
|
|
405
|
+
self._message,
|
|
406
|
+
duration,
|
|
407
|
+
self._client._extend_lease, # noqa: SLF001
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def extend(self, duration: Duration) -> None:
|
|
411
|
+
"""Sync flavor of :meth:`extend_async`."""
|
|
412
|
+
if self._message.metadata.receipt_handle is None:
|
|
413
|
+
return
|
|
414
|
+
retry_message_after_sync(
|
|
415
|
+
self._message,
|
|
416
|
+
duration,
|
|
417
|
+
lambda message, duration: iter_coroutine(
|
|
418
|
+
self._client._extend_lease(message, duration) # noqa: SLF001
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _prime_oidc_token_cache() -> None:
|
|
424
|
+
try:
|
|
425
|
+
get_vercel_oidc_token_sync()
|
|
426
|
+
except Exception as exc: # noqa: BLE001
|
|
427
|
+
debug_log(
|
|
428
|
+
"lease.oidc_cache_prime_failed",
|
|
429
|
+
exception_class=exc.__class__.__name__,
|
|
430
|
+
exception_message=str(exc),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def retry_message_after_async(
|
|
435
|
+
message: Message[Any] | MessageMetadata,
|
|
436
|
+
duration: Duration,
|
|
437
|
+
extend_lease: AsyncExtendMessageLease,
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Request redelivery of a leased message after ``duration``.
|
|
440
|
+
|
|
441
|
+
Retries transient failures and tolerates an already-missing lease with a
|
|
442
|
+
warning: the message can be settled or redelivered elsewhere before this
|
|
443
|
+
lands (lease expiry, concurrent consumer), in which case the requested
|
|
444
|
+
visibility no longer applies but the message itself is not lost.
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
await retry_async_follow_up(
|
|
448
|
+
lambda: extend_lease(message, duration),
|
|
449
|
+
event_prefix="visibility",
|
|
450
|
+
)
|
|
451
|
+
except MessageNotFoundError:
|
|
452
|
+
_warn_lease_gone(message)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def retry_message_after_sync(
|
|
456
|
+
message: Message[Any] | MessageMetadata,
|
|
457
|
+
duration: Duration,
|
|
458
|
+
extend_lease: ExtendMessageLease,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Sync flavor of :func:`retry_message_after_async`."""
|
|
461
|
+
try:
|
|
462
|
+
retry_sync_follow_up(
|
|
463
|
+
lambda: extend_lease(message, duration),
|
|
464
|
+
event_prefix="visibility",
|
|
465
|
+
)
|
|
466
|
+
except MessageNotFoundError:
|
|
467
|
+
_warn_lease_gone(message)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _warn_lease_gone(message: Message[Any] | MessageMetadata) -> None:
|
|
471
|
+
metadata = message.metadata if isinstance(message, Message) else message
|
|
472
|
+
logger.warning(
|
|
473
|
+
"queue lease for message %s on topic %s/%s no longer exists; "
|
|
474
|
+
"skipping requested visibility update",
|
|
475
|
+
metadata.message_id,
|
|
476
|
+
metadata.topic,
|
|
477
|
+
metadata.consumer_group,
|
|
478
|
+
)
|
|
479
|
+
debug_log(
|
|
480
|
+
"visibility.lease_gone",
|
|
481
|
+
message_id=metadata.message_id,
|
|
482
|
+
topic=metadata.topic,
|
|
483
|
+
consumer_group=metadata.consumer_group,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _send_lease_command(command: _LeaseExtensionCommand) -> None:
|
|
488
|
+
command_send = _lease_worker_state.command_send
|
|
489
|
+
worker_token = _lease_worker_state.worker_token
|
|
490
|
+
if command_send is None or worker_token is None:
|
|
491
|
+
raise RuntimeError("lease extension worker is not ready")
|
|
492
|
+
try:
|
|
493
|
+
anyio.from_thread.run_sync(command_send.send_nowait, command, token=worker_token)
|
|
494
|
+
except anyio.WouldBlock as exc:
|
|
495
|
+
raise RuntimeError("lease extension worker command buffer is full") from exc
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _send_lease_extension_start(request: _LeaseExtensionRequest) -> None:
|
|
499
|
+
scheduled_event = threading.Event()
|
|
500
|
+
command = _LeaseExtensionStart(request=request, scheduled_event=scheduled_event)
|
|
501
|
+
_send_lease_command(command)
|
|
502
|
+
if not scheduled_event.wait(_LEASE_START_WAIT_TIMEOUT_SECONDS):
|
|
503
|
+
_send_best_effort_start_timeout_stop(request.token)
|
|
504
|
+
raise TimeoutError("timed out waiting for queue lease renewal to start")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
async def _send_lease_extension_start_async(request: _LeaseExtensionRequest) -> None:
|
|
508
|
+
scheduled_event = anyio.Event()
|
|
509
|
+
command = _LeaseExtensionStart(
|
|
510
|
+
request=request,
|
|
511
|
+
scheduled_event=scheduled_event,
|
|
512
|
+
event_loop_token=current_token(),
|
|
513
|
+
)
|
|
514
|
+
send_errors = _send_lease_extension_start_command_async(command)
|
|
515
|
+
try:
|
|
516
|
+
with anyio.fail_after(_LEASE_START_WAIT_TIMEOUT_SECONDS):
|
|
517
|
+
await scheduled_event.wait()
|
|
518
|
+
except TimeoutError:
|
|
519
|
+
await to_thread.run_sync(_send_best_effort_start_timeout_stop, request.token)
|
|
520
|
+
raise
|
|
521
|
+
if send_errors:
|
|
522
|
+
raise send_errors[0]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _send_lease_extension_start_command_async(
|
|
526
|
+
command: _LeaseExtensionStart,
|
|
527
|
+
) -> list[Exception]:
|
|
528
|
+
command_send = _lease_worker_state.command_send
|
|
529
|
+
worker_token = _lease_worker_state.worker_token
|
|
530
|
+
if command_send is None or worker_token is None:
|
|
531
|
+
raise RuntimeError("lease extension worker is not ready")
|
|
532
|
+
caller_token = current_token()
|
|
533
|
+
send_errors: list[Exception] = []
|
|
534
|
+
|
|
535
|
+
def send_nowait() -> None:
|
|
536
|
+
try:
|
|
537
|
+
command_send.send_nowait(command)
|
|
538
|
+
except anyio.WouldBlock as exc:
|
|
539
|
+
error = RuntimeError("lease extension worker command buffer is full")
|
|
540
|
+
error.__cause__ = exc
|
|
541
|
+
send_errors.append(error)
|
|
542
|
+
_schedule_event_loop_callback(command.scheduled_event.set, caller_token)
|
|
543
|
+
except Exception as exc: # noqa: BLE001
|
|
544
|
+
send_errors.append(exc)
|
|
545
|
+
_schedule_event_loop_callback(command.scheduled_event.set, caller_token)
|
|
546
|
+
|
|
547
|
+
_schedule_event_loop_callback(send_nowait, worker_token)
|
|
548
|
+
return send_errors
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _send_best_effort_start_timeout_stop(token: _LeaseRenewalToken) -> None:
|
|
552
|
+
try:
|
|
553
|
+
_send_lease_command(_LeaseExtensionStop(token=token))
|
|
554
|
+
except Exception as exc:
|
|
555
|
+
logger.debug("failed to cancel timed out queue lease renewal start", exc_info=exc)
|
|
556
|
+
debug_log(
|
|
557
|
+
"lease.start_timeout_stop_failed",
|
|
558
|
+
exception_class=exc.__class__.__name__,
|
|
559
|
+
exception_message=str(exc),
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
async def _send_best_effort_async_start_failure_stop(token: _LeaseRenewalToken) -> None:
|
|
564
|
+
with anyio.CancelScope(shield=True):
|
|
565
|
+
try:
|
|
566
|
+
await to_thread.run_sync(_send_best_effort_start_timeout_stop, token)
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
logger.debug("failed to cancel failed queue lease renewal start", exc_info=exc)
|
|
569
|
+
debug_log(
|
|
570
|
+
"lease.start_failure_stop_failed",
|
|
571
|
+
exception_class=exc.__class__.__name__,
|
|
572
|
+
exception_message=str(exc),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _log_best_effort_lease_stop_failure(exc: BaseException) -> None:
|
|
577
|
+
logger.debug("failed to send best-effort queue lease renewal stop", exc_info=exc)
|
|
578
|
+
debug_log(
|
|
579
|
+
"lease.best_effort_stop_failed",
|
|
580
|
+
exception_class=exc.__class__.__name__,
|
|
581
|
+
exception_message=str(exc),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _send_lease_extension_stop(token: _LeaseRenewalToken) -> bool:
|
|
586
|
+
deadline = time.monotonic() + _LEASE_STOP_WAIT_TIMEOUT_SECONDS
|
|
587
|
+
done_event = threading.Event()
|
|
588
|
+
_send_lease_command(_LeaseExtensionStop(token=token, done_event=done_event))
|
|
589
|
+
return done_event.wait(max(0.0, deadline - time.monotonic()))
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
async def _send_lease_extension_stop_async(token: _LeaseRenewalToken) -> bool:
|
|
593
|
+
done_event = anyio.Event()
|
|
594
|
+
await to_thread.run_sync(
|
|
595
|
+
_send_lease_command,
|
|
596
|
+
_LeaseExtensionStop(
|
|
597
|
+
token=token,
|
|
598
|
+
done_event=done_event,
|
|
599
|
+
event_loop_token=current_token(),
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
with anyio.move_on_after(_LEASE_STOP_WAIT_TIMEOUT_SECONDS) as scope:
|
|
603
|
+
await done_event.wait()
|
|
604
|
+
return not scope.cancel_called
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _ensure_lease_renewal_thread() -> None:
|
|
608
|
+
if _lease_worker_state.started:
|
|
609
|
+
_wait_for_lease_worker_ready()
|
|
610
|
+
return
|
|
611
|
+
wait_for_ready = False
|
|
612
|
+
with _lease_worker_lock:
|
|
613
|
+
if _lease_worker_state.started:
|
|
614
|
+
wait_for_ready = True
|
|
615
|
+
else:
|
|
616
|
+
_lease_worker_ready.clear()
|
|
617
|
+
_lease_worker_state.startup_error = None
|
|
618
|
+
thread = threading.Thread(
|
|
619
|
+
target=_lease_renewal_worker_thread,
|
|
620
|
+
name="vercel-queue-lease-renewal",
|
|
621
|
+
daemon=True,
|
|
622
|
+
)
|
|
623
|
+
debug_log("lease.worker_start", thread_name=thread.name)
|
|
624
|
+
thread.start()
|
|
625
|
+
_lease_worker_state.thread = thread
|
|
626
|
+
_lease_worker_state.started = True
|
|
627
|
+
wait_for_ready = True
|
|
628
|
+
if wait_for_ready:
|
|
629
|
+
_wait_for_lease_worker_ready()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _wait_for_lease_worker_ready() -> None:
|
|
633
|
+
thread = _lease_worker_state.thread
|
|
634
|
+
if not _lease_worker_ready.wait(_LEASE_WORKER_START_TIMEOUT_SECONDS):
|
|
635
|
+
_mark_lease_worker_stopped(thread)
|
|
636
|
+
raise RuntimeError("lease extension worker did not start")
|
|
637
|
+
startup_error = _lease_worker_state.startup_error
|
|
638
|
+
if startup_error is not None:
|
|
639
|
+
_mark_lease_worker_stopped(thread)
|
|
640
|
+
raise RuntimeError("lease extension worker failed to start") from startup_error
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _lease_renewal_worker_thread() -> None:
|
|
644
|
+
try:
|
|
645
|
+
_run_lease_renewal_worker_thread()
|
|
646
|
+
except BaseException as exc:
|
|
647
|
+
_lease_worker_state.startup_error = exc
|
|
648
|
+
_lease_worker_ready.set()
|
|
649
|
+
raise
|
|
650
|
+
finally:
|
|
651
|
+
_mark_lease_worker_stopped(threading.current_thread())
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _mark_lease_worker_stopped(thread: threading.Thread | None = None) -> None:
|
|
655
|
+
with _lease_worker_lock:
|
|
656
|
+
if thread is not None and _lease_worker_state.thread is not thread:
|
|
657
|
+
return
|
|
658
|
+
_lease_worker_state.command_send = None
|
|
659
|
+
_lease_worker_state.worker_token = None
|
|
660
|
+
_lease_worker_state.thread = None
|
|
661
|
+
_lease_worker_state.started = False
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _run_lease_renewal_worker_thread() -> None:
|
|
665
|
+
anyio.run(_run_lease_renewal_worker_async)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
async def _run_lease_renewal_worker_async() -> None:
|
|
669
|
+
command_send, command_receive = anyio.create_memory_object_stream[_LeaseExtensionCommand](
|
|
670
|
+
_LEASE_COMMAND_BUFFER_SIZE
|
|
671
|
+
)
|
|
672
|
+
_lease_worker_state.command_send = command_send
|
|
673
|
+
_lease_worker_state.worker_token = current_token()
|
|
674
|
+
_lease_worker_ready.set()
|
|
675
|
+
debug_log("lease.worker_ready")
|
|
676
|
+
try:
|
|
677
|
+
await _lease_worker_async(command_receive)
|
|
678
|
+
finally:
|
|
679
|
+
command_send.close()
|
|
680
|
+
await command_receive.aclose()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
async def _lease_worker_async(
|
|
684
|
+
commands: ObjectReceiveStream[_LeaseExtensionCommand],
|
|
685
|
+
) -> None:
|
|
686
|
+
state = _LeaseWorkerRuntimeState(
|
|
687
|
+
active={},
|
|
688
|
+
stopped=OrderedDict(),
|
|
689
|
+
)
|
|
690
|
+
async with anyio.create_task_group() as task_group:
|
|
691
|
+
while True:
|
|
692
|
+
try:
|
|
693
|
+
command = await _receive_lease_command(commands)
|
|
694
|
+
except anyio.EndOfStream:
|
|
695
|
+
task_group.cancel_scope.cancel()
|
|
696
|
+
return
|
|
697
|
+
if command is None:
|
|
698
|
+
continue
|
|
699
|
+
if await _handle_lease_worker_command(command, state, task_group):
|
|
700
|
+
task_group.cancel_scope.cancel()
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
async def _handle_lease_worker_command(
|
|
705
|
+
command: _LeaseExtensionCommand,
|
|
706
|
+
state: _LeaseWorkerRuntimeState,
|
|
707
|
+
task_group: TaskGroup,
|
|
708
|
+
) -> bool:
|
|
709
|
+
if isinstance(command, _LeaseExtensionStart):
|
|
710
|
+
_handle_lease_extension_start(command, state, task_group)
|
|
711
|
+
return False
|
|
712
|
+
if isinstance(command, _LeaseExtensionStop):
|
|
713
|
+
await _handle_lease_extension_stop(command, state)
|
|
714
|
+
return False
|
|
715
|
+
|
|
716
|
+
debug_log("lease.worker_shutdown")
|
|
717
|
+
return True
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _handle_lease_extension_start(
|
|
721
|
+
command: _LeaseExtensionStart,
|
|
722
|
+
state: _LeaseWorkerRuntimeState,
|
|
723
|
+
task_group: TaskGroup,
|
|
724
|
+
) -> None:
|
|
725
|
+
try:
|
|
726
|
+
_start_lease_renewal_task(command.request, state, task_group)
|
|
727
|
+
finally:
|
|
728
|
+
_signal_lease_start_scheduled(command)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _signal_lease_start_scheduled(command: _LeaseExtensionStart) -> None:
|
|
732
|
+
try:
|
|
733
|
+
_schedule_lease_start_signal(command)
|
|
734
|
+
except Exception as exc:
|
|
735
|
+
logger.debug("failed to signal queue lease renewal start scheduling", exc_info=exc)
|
|
736
|
+
debug_log(
|
|
737
|
+
"lease.start_signal_failed",
|
|
738
|
+
exception_class=exc.__class__.__name__,
|
|
739
|
+
exception_message=str(exc),
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _schedule_lease_start_signal(command: _LeaseExtensionStart) -> None:
|
|
744
|
+
if command.event_loop_token is None:
|
|
745
|
+
command.scheduled_event.set()
|
|
746
|
+
return
|
|
747
|
+
_schedule_event_loop_callback(command.scheduled_event.set, command.event_loop_token)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _schedule_event_loop_callback(
|
|
751
|
+
callback: Callable[[], None],
|
|
752
|
+
event_loop_token: EventLoopToken,
|
|
753
|
+
) -> None:
|
|
754
|
+
native_token = event_loop_token.native_token
|
|
755
|
+
if call_soon_threadsafe := getattr(native_token, "call_soon_threadsafe", None):
|
|
756
|
+
call_soon_threadsafe(callback)
|
|
757
|
+
return
|
|
758
|
+
if run_sync_soon := getattr(native_token, "run_sync_soon", None):
|
|
759
|
+
run_sync_soon(callback)
|
|
760
|
+
return
|
|
761
|
+
anyio.from_thread.run_sync(callback, token=event_loop_token)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _start_lease_renewal_task(
|
|
765
|
+
request: _LeaseExtensionRequest,
|
|
766
|
+
state: _LeaseWorkerRuntimeState,
|
|
767
|
+
task_group: TaskGroup,
|
|
768
|
+
) -> None:
|
|
769
|
+
if request.token in state.stopped:
|
|
770
|
+
state.stopped.pop(request.token, None)
|
|
771
|
+
return
|
|
772
|
+
if request.token in state.active:
|
|
773
|
+
return
|
|
774
|
+
renewal_task = _LeaseRenewalTask(
|
|
775
|
+
request=request,
|
|
776
|
+
cancel_scope=anyio.CancelScope(),
|
|
777
|
+
stop_waiters=[],
|
|
778
|
+
)
|
|
779
|
+
state.active[request.token] = renewal_task
|
|
780
|
+
task_group.start_soon(_run_lease_renewal_task, renewal_task, state)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
async def _handle_lease_extension_stop(
|
|
784
|
+
command: _LeaseExtensionStop,
|
|
785
|
+
state: _LeaseWorkerRuntimeState,
|
|
786
|
+
) -> None:
|
|
787
|
+
renewal_task = state.active.pop(command.token, None)
|
|
788
|
+
if renewal_task is not None:
|
|
789
|
+
if command.done_send is not None or command.done_event is not None:
|
|
790
|
+
renewal_task.stop_waiters.append(command)
|
|
791
|
+
renewal_task.cancel_scope.cancel()
|
|
792
|
+
return
|
|
793
|
+
|
|
794
|
+
_remember_stopped_lease_token(command.token, state)
|
|
795
|
+
await _signal_lease_stop_complete(command)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _remember_stopped_lease_token(
|
|
799
|
+
token: _LeaseRenewalToken,
|
|
800
|
+
state: _LeaseWorkerRuntimeState,
|
|
801
|
+
) -> None:
|
|
802
|
+
state.stopped[token] = None
|
|
803
|
+
state.stopped.move_to_end(token)
|
|
804
|
+
while len(state.stopped) > _STOPPED_LEASE_TOKEN_CACHE_SIZE:
|
|
805
|
+
state.stopped.popitem(last=False)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
async def _signal_lease_stop_complete(command: _LeaseExtensionStop) -> None:
|
|
809
|
+
if command.done_send is not None:
|
|
810
|
+
try:
|
|
811
|
+
await command.done_send.send(None)
|
|
812
|
+
except Exception as exc: # noqa: BLE001
|
|
813
|
+
_log_lease_stop_signal_failure(exc)
|
|
814
|
+
if command.done_event is not None:
|
|
815
|
+
try:
|
|
816
|
+
if command.event_loop_token is None:
|
|
817
|
+
command.done_event.set()
|
|
818
|
+
else:
|
|
819
|
+
_schedule_event_loop_callback(command.done_event.set, command.event_loop_token)
|
|
820
|
+
except Exception as exc: # noqa: BLE001
|
|
821
|
+
_log_lease_stop_signal_failure(exc)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _log_lease_stop_signal_failure(exc: BaseException) -> None:
|
|
825
|
+
logger.debug("failed to signal queue lease renewal stop completion", exc_info=exc)
|
|
826
|
+
debug_log(
|
|
827
|
+
"lease.stop_signal_failed",
|
|
828
|
+
exception_class=exc.__class__.__name__,
|
|
829
|
+
exception_message=str(exc),
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def reset_lease_renewal_worker_for_tests() -> None:
|
|
834
|
+
"""Stop the background lease renewal worker and clear cached clients."""
|
|
835
|
+
thread = _lease_worker_state.thread
|
|
836
|
+
if _lease_worker_state.command_send is not None:
|
|
837
|
+
_send_lease_command(_LeaseWorkerShutdown())
|
|
838
|
+
if thread is not None:
|
|
839
|
+
thread.join(timeout=5)
|
|
840
|
+
with _lease_worker_lock:
|
|
841
|
+
_lease_worker_state.command_send = None
|
|
842
|
+
_lease_worker_state.worker_token = None
|
|
843
|
+
_lease_worker_state.thread = None
|
|
844
|
+
_lease_worker_state.started = False
|
|
845
|
+
_lease_worker_ready.clear()
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
async def _receive_lease_command(
|
|
849
|
+
commands: ObjectReceiveStream[_LeaseExtensionCommand],
|
|
850
|
+
) -> _LeaseExtensionCommand:
|
|
851
|
+
return await commands.receive()
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _is_client_error(exc: QueueError) -> bool:
|
|
855
|
+
if isinstance(exc, ThrottledError):
|
|
856
|
+
return False
|
|
857
|
+
status_code = exc.status_code
|
|
858
|
+
if status_code == 408:
|
|
859
|
+
return False
|
|
860
|
+
return status_code is not None and 400 <= status_code < 500
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
async def _run_lease_renewal_task(
|
|
864
|
+
renewal_task: _LeaseRenewalTask,
|
|
865
|
+
state: _LeaseWorkerRuntimeState,
|
|
866
|
+
) -> None:
|
|
867
|
+
request = renewal_task.request
|
|
868
|
+
try:
|
|
869
|
+
with renewal_task.cancel_scope:
|
|
870
|
+
while state.active.get(request.token) is renewal_task:
|
|
871
|
+
await anyio.sleep(max(0.0, request.next_extension_at - time.monotonic()))
|
|
872
|
+
if state.active.get(request.token) is not renewal_task:
|
|
873
|
+
return
|
|
874
|
+
await _run_lease_extension(request, _LeaseTaskActiveMap(state.active))
|
|
875
|
+
finally:
|
|
876
|
+
with anyio.CancelScope(shield=True):
|
|
877
|
+
if state.active.get(request.token) is renewal_task:
|
|
878
|
+
state.active.pop(request.token, None)
|
|
879
|
+
for stop_command in renewal_task.stop_waiters:
|
|
880
|
+
await _signal_lease_stop_complete(stop_command)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class _LeaseTaskActiveMap(MutableMapping[_LeaseRenewalToken, _LeaseExtensionRequest]):
|
|
884
|
+
def __init__(self, active: dict[_LeaseRenewalToken, _LeaseRenewalTask]) -> None:
|
|
885
|
+
self._active = active
|
|
886
|
+
|
|
887
|
+
def __getitem__(self, token: _LeaseRenewalToken) -> _LeaseExtensionRequest:
|
|
888
|
+
return self._active[token].request
|
|
889
|
+
|
|
890
|
+
def __setitem__(
|
|
891
|
+
self,
|
|
892
|
+
token: _LeaseRenewalToken,
|
|
893
|
+
request: _LeaseExtensionRequest,
|
|
894
|
+
) -> None:
|
|
895
|
+
self._active[token].request = request
|
|
896
|
+
|
|
897
|
+
def __delitem__(self, token: _LeaseRenewalToken) -> None:
|
|
898
|
+
del self._active[token]
|
|
899
|
+
|
|
900
|
+
def __iter__(self) -> Iterator[_LeaseRenewalToken]:
|
|
901
|
+
return iter(self._active)
|
|
902
|
+
|
|
903
|
+
def __len__(self) -> int:
|
|
904
|
+
return len(self._active)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
async def _run_lease_extension(
|
|
908
|
+
request: _LeaseExtensionRequest,
|
|
909
|
+
active: MutableMapping[_LeaseRenewalToken, _LeaseExtensionRequest],
|
|
910
|
+
) -> None:
|
|
911
|
+
if request.token not in active:
|
|
912
|
+
return
|
|
913
|
+
debug_log(
|
|
914
|
+
"lease.extension_attempt",
|
|
915
|
+
message_id=request.message.metadata.message_id,
|
|
916
|
+
topic=request.message.metadata.topic,
|
|
917
|
+
consumer_group=request.message.metadata.consumer_group,
|
|
918
|
+
lease_seconds=request.lease_seconds,
|
|
919
|
+
)
|
|
920
|
+
try:
|
|
921
|
+
await request.client._renew_lease( # noqa: SLF001
|
|
922
|
+
request.message,
|
|
923
|
+
request.lease_seconds,
|
|
924
|
+
)
|
|
925
|
+
except QueueError as exc:
|
|
926
|
+
if _is_client_error(exc):
|
|
927
|
+
active.pop(request.token, None)
|
|
928
|
+
debug_log(
|
|
929
|
+
"lease.client_error_stop",
|
|
930
|
+
message_id=request.message.metadata.message_id,
|
|
931
|
+
topic=request.message.metadata.topic,
|
|
932
|
+
consumer_group=request.message.metadata.consumer_group,
|
|
933
|
+
exception_class=exc.__class__.__name__,
|
|
934
|
+
exception_message=str(exc),
|
|
935
|
+
)
|
|
936
|
+
return
|
|
937
|
+
if request.token not in active:
|
|
938
|
+
return
|
|
939
|
+
logger.warning(
|
|
940
|
+
"transient queue lease renewal failure for message %s on topic %s/%s: %s",
|
|
941
|
+
request.message.metadata.message_id,
|
|
942
|
+
request.message.metadata.topic,
|
|
943
|
+
request.message.metadata.consumer_group,
|
|
944
|
+
exc,
|
|
945
|
+
)
|
|
946
|
+
retry_delay = _lease_extension_retry_delay(exc, request.lease_seconds)
|
|
947
|
+
request.next_extension_at = time.monotonic() + retry_delay
|
|
948
|
+
debug_log(
|
|
949
|
+
"lease.transient_retry_scheduled",
|
|
950
|
+
message_id=request.message.metadata.message_id,
|
|
951
|
+
topic=request.message.metadata.topic,
|
|
952
|
+
consumer_group=request.message.metadata.consumer_group,
|
|
953
|
+
retry_delay_seconds=retry_delay,
|
|
954
|
+
exception_class=exc.__class__.__name__,
|
|
955
|
+
exception_message=str(exc),
|
|
956
|
+
)
|
|
957
|
+
return
|
|
958
|
+
except Exception as exc:
|
|
959
|
+
if request.token not in active:
|
|
960
|
+
return
|
|
961
|
+
logger.warning(
|
|
962
|
+
"unexpected queue lease renewal failure for message %s on topic %s/%s",
|
|
963
|
+
request.message.metadata.message_id,
|
|
964
|
+
request.message.metadata.topic,
|
|
965
|
+
request.message.metadata.consumer_group,
|
|
966
|
+
exc_info=exc,
|
|
967
|
+
)
|
|
968
|
+
retry_delay = _lease_extension_retry_delay(exc, request.lease_seconds)
|
|
969
|
+
request.next_extension_at = time.monotonic() + retry_delay
|
|
970
|
+
debug_log(
|
|
971
|
+
"lease.unexpected_retry_scheduled",
|
|
972
|
+
message_id=request.message.metadata.message_id,
|
|
973
|
+
topic=request.message.metadata.topic,
|
|
974
|
+
consumer_group=request.message.metadata.consumer_group,
|
|
975
|
+
retry_delay_seconds=retry_delay,
|
|
976
|
+
exception_class=exc.__class__.__name__,
|
|
977
|
+
exception_message=str(exc),
|
|
978
|
+
)
|
|
979
|
+
return
|
|
980
|
+
if request.token not in active:
|
|
981
|
+
return
|
|
982
|
+
request.next_extension_at = time.monotonic() + _renewal_interval_seconds(request.lease_seconds)
|
|
983
|
+
debug_log(
|
|
984
|
+
"lease.extension_success",
|
|
985
|
+
message_id=request.message.metadata.message_id,
|
|
986
|
+
topic=request.message.metadata.topic,
|
|
987
|
+
consumer_group=request.message.metadata.consumer_group,
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _lease_extension_retry_delay(exc: BaseException, lease_seconds: int) -> float:
|
|
992
|
+
if isinstance(exc, ThrottledError) and exc.retry_after is not None:
|
|
993
|
+
retry_after = max(1.0, float(exc.retry_after))
|
|
994
|
+
return min(retry_after, _renewal_interval_seconds(lease_seconds))
|
|
995
|
+
return _TRANSIENT_LEASE_RETRY_SECONDS
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
async def finalize_payload_async(payload: Any) -> None:
|
|
999
|
+
if isinstance(payload, (AsyncStreamPayload, AsyncTextStreamPayload)):
|
|
1000
|
+
await payload.afinalize()
|
|
1001
|
+
elif isinstance(payload, (SyncStreamPayload, SyncTextStreamPayload)):
|
|
1002
|
+
payload.finalize()
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def finalize_payload_sync(payload: Any) -> None:
|
|
1006
|
+
if isinstance(payload, (SyncStreamPayload, SyncTextStreamPayload)):
|
|
1007
|
+
payload.finalize()
|