redqueue 0.10.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.
- redqueue/__init__.py +72 -0
- redqueue/_version.py +6 -0
- redqueue/async_client.py +194 -0
- redqueue/backends/__init__.py +20 -0
- redqueue/backends/async_delay.py +242 -0
- redqueue/backends/async_list.py +308 -0
- redqueue/backends/async_stream.py +394 -0
- redqueue/backends/base.py +103 -0
- redqueue/backends/delay.py +243 -0
- redqueue/backends/list.py +303 -0
- redqueue/backends/stream.py +370 -0
- redqueue/client.py +168 -0
- redqueue/compat.py +184 -0
- redqueue/config.py +155 -0
- redqueue/exceptions.py +167 -0
- redqueue/message.py +67 -0
- redqueue/monitoring.py +112 -0
- redqueue/serialization.py +79 -0
- redqueue-0.10.0.dist-info/METADATA +312 -0
- redqueue-0.10.0.dist-info/RECORD +23 -0
- redqueue-0.10.0.dist-info/WHEEL +4 -0
- redqueue-0.10.0.dist-info/licenses/LICENSE +158 -0
- redqueue-0.10.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Synchronous Redis Streams backend."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
from redqueue.backends.base import BaseMessageBackend
|
|
11
|
+
from redqueue.compat import RedisCapabilities, RedisVersion
|
|
12
|
+
from redqueue.config import QueueConfig
|
|
13
|
+
from redqueue.exceptions import AckError, BackendUnavailableError, RetryExceededError
|
|
14
|
+
from redqueue.message import Message, new_message_id
|
|
15
|
+
from redqueue.monitoring import MonitoringEventType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SyncStreamRedis(Protocol):
|
|
19
|
+
"""Redis command subset required by the synchronous Streams backend."""
|
|
20
|
+
|
|
21
|
+
def xadd(self, name: str, fields: dict[str, bytes | str], id: str = "*") -> str: ...
|
|
22
|
+
|
|
23
|
+
def xgroup_create(
|
|
24
|
+
self,
|
|
25
|
+
name: str,
|
|
26
|
+
groupname: str,
|
|
27
|
+
id: str = "0",
|
|
28
|
+
mkstream: bool = True,
|
|
29
|
+
) -> bool: ...
|
|
30
|
+
|
|
31
|
+
def xreadgroup(
|
|
32
|
+
self,
|
|
33
|
+
groupname: str,
|
|
34
|
+
consumername: str,
|
|
35
|
+
streams: dict[str, str],
|
|
36
|
+
count: int | None = None,
|
|
37
|
+
block: int | None = None,
|
|
38
|
+
) -> list[Any]: ...
|
|
39
|
+
|
|
40
|
+
def xack(self, name: str, groupname: str, *ids: str) -> int: ...
|
|
41
|
+
|
|
42
|
+
def xautoclaim(
|
|
43
|
+
self,
|
|
44
|
+
name: str,
|
|
45
|
+
groupname: str,
|
|
46
|
+
consumername: str,
|
|
47
|
+
min_idle_time: int,
|
|
48
|
+
start_id: str,
|
|
49
|
+
count: int | None = None,
|
|
50
|
+
) -> Any: ...
|
|
51
|
+
|
|
52
|
+
def xpending_range(
|
|
53
|
+
self,
|
|
54
|
+
name: str,
|
|
55
|
+
groupname: str,
|
|
56
|
+
min: str,
|
|
57
|
+
max: str,
|
|
58
|
+
count: int,
|
|
59
|
+
) -> list[Any]: ...
|
|
60
|
+
|
|
61
|
+
def xclaim(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
groupname: str,
|
|
65
|
+
consumername: str,
|
|
66
|
+
min_idle_time: int,
|
|
67
|
+
message_ids: list[str],
|
|
68
|
+
) -> list[Any]: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StreamBackend(BaseMessageBackend):
|
|
72
|
+
"""Reliable queue backend implemented with Redis Streams."""
|
|
73
|
+
|
|
74
|
+
backend_name = "stream"
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
redis: SyncStreamRedis,
|
|
79
|
+
config: QueueConfig,
|
|
80
|
+
capabilities: RedisCapabilities,
|
|
81
|
+
) -> None:
|
|
82
|
+
capabilities.require_streams()
|
|
83
|
+
super().__init__(config)
|
|
84
|
+
self.redis = redis
|
|
85
|
+
self.capabilities = capabilities
|
|
86
|
+
self._ensure_group()
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def for_modern_redis(
|
|
90
|
+
cls,
|
|
91
|
+
redis: SyncStreamRedis,
|
|
92
|
+
config: QueueConfig,
|
|
93
|
+
) -> StreamBackend:
|
|
94
|
+
return cls(redis, config, RedisCapabilities(RedisVersion(7, 0, 0)))
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def stream_key(self) -> str:
|
|
98
|
+
return self.config.key("stream")
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def dead_key(self) -> str:
|
|
102
|
+
return self.config.key("dead")
|
|
103
|
+
|
|
104
|
+
def publish(
|
|
105
|
+
self,
|
|
106
|
+
payload: Any,
|
|
107
|
+
*,
|
|
108
|
+
headers: dict[str, Any] | None = None,
|
|
109
|
+
message_id: str | None = None,
|
|
110
|
+
) -> str:
|
|
111
|
+
message = Message(
|
|
112
|
+
id=message_id or new_message_id(),
|
|
113
|
+
queue=self.config.queue,
|
|
114
|
+
payload=payload,
|
|
115
|
+
headers=headers or {},
|
|
116
|
+
backend=self.backend_name,
|
|
117
|
+
)
|
|
118
|
+
self._publish_message(message)
|
|
119
|
+
return message.id
|
|
120
|
+
|
|
121
|
+
def consume(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
timeout: float | None = None,
|
|
125
|
+
batch_size: int = 1,
|
|
126
|
+
) -> Message | list[Message] | None:
|
|
127
|
+
block = int(timeout * 1000) if timeout is not None else None
|
|
128
|
+
response = self._execute(
|
|
129
|
+
"redis.xreadgroup",
|
|
130
|
+
self.redis.xreadgroup,
|
|
131
|
+
self.config.consumer_group,
|
|
132
|
+
self._consumer_name(),
|
|
133
|
+
{self.stream_key: ">"},
|
|
134
|
+
batch_size,
|
|
135
|
+
block,
|
|
136
|
+
)
|
|
137
|
+
messages = self._parse_read_response(response)
|
|
138
|
+
if batch_size <= 1:
|
|
139
|
+
if not messages:
|
|
140
|
+
return None
|
|
141
|
+
self._emit(MonitoringEventType.MESSAGE_CONSUMED, messages[0])
|
|
142
|
+
return messages[0]
|
|
143
|
+
for message in messages:
|
|
144
|
+
self._emit(MonitoringEventType.MESSAGE_CONSUMED, message)
|
|
145
|
+
return messages
|
|
146
|
+
|
|
147
|
+
def ack(self, message: Message) -> None:
|
|
148
|
+
if not message.raw_id:
|
|
149
|
+
raise AckError(
|
|
150
|
+
"stream message is missing raw Redis stream id",
|
|
151
|
+
action="message.ack",
|
|
152
|
+
queue=self.config.queue,
|
|
153
|
+
details={"message_id": message.id},
|
|
154
|
+
)
|
|
155
|
+
removed = self._execute(
|
|
156
|
+
"redis.xack",
|
|
157
|
+
self.redis.xack,
|
|
158
|
+
self.stream_key,
|
|
159
|
+
self.config.consumer_group,
|
|
160
|
+
message.raw_id,
|
|
161
|
+
)
|
|
162
|
+
if removed < 1:
|
|
163
|
+
raise AckError(
|
|
164
|
+
"stream message was not acknowledged",
|
|
165
|
+
action="message.ack",
|
|
166
|
+
queue=self.config.queue,
|
|
167
|
+
details={"message_id": message.id, "raw_id": message.raw_id},
|
|
168
|
+
)
|
|
169
|
+
self._emit(MonitoringEventType.MESSAGE_ACKED, message)
|
|
170
|
+
|
|
171
|
+
def nack(self, message: Message, *, requeue: bool = True) -> None:
|
|
172
|
+
if requeue:
|
|
173
|
+
self.publish(
|
|
174
|
+
message.payload,
|
|
175
|
+
headers=message.headers,
|
|
176
|
+
message_id=message.id,
|
|
177
|
+
)
|
|
178
|
+
self.ack(message)
|
|
179
|
+
else:
|
|
180
|
+
self._move_to_dead(message)
|
|
181
|
+
self._emit(
|
|
182
|
+
MonitoringEventType.MESSAGE_NACKED,
|
|
183
|
+
message,
|
|
184
|
+
attributes={"requeue": requeue},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def retry(
|
|
188
|
+
self,
|
|
189
|
+
message: Message,
|
|
190
|
+
*,
|
|
191
|
+
delay: float | None = None,
|
|
192
|
+
reason: str | None = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
if message.attempts >= self.config.retry.max_retries:
|
|
195
|
+
self._move_to_dead(message)
|
|
196
|
+
self._emit(
|
|
197
|
+
MonitoringEventType.MESSAGE_DEAD_LETTERED,
|
|
198
|
+
message,
|
|
199
|
+
attributes={"reason": reason, "attempts": message.attempts},
|
|
200
|
+
)
|
|
201
|
+
raise RetryExceededError(
|
|
202
|
+
"stream message exceeded max retries and was moved to dead letter",
|
|
203
|
+
action="message.retry",
|
|
204
|
+
queue=self.config.queue,
|
|
205
|
+
details={
|
|
206
|
+
"message_id": message.id,
|
|
207
|
+
"attempts": message.attempts,
|
|
208
|
+
"max_retries": self.config.retry.max_retries,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
retried = message.with_attempt()
|
|
212
|
+
self._publish_message(retried)
|
|
213
|
+
self.ack(message)
|
|
214
|
+
self._emit(
|
|
215
|
+
MonitoringEventType.MESSAGE_RETRIED,
|
|
216
|
+
retried,
|
|
217
|
+
attributes={"delay": delay, "reason": reason},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def recover_pending(self, *, min_idle_ms: int, limit: int = 100) -> list[Message]:
|
|
221
|
+
if not self.capabilities.supports_streams_auto_claim:
|
|
222
|
+
pending = self._execute(
|
|
223
|
+
"redis.xpending_range",
|
|
224
|
+
self.redis.xpending_range,
|
|
225
|
+
self.stream_key,
|
|
226
|
+
self.config.consumer_group,
|
|
227
|
+
"-",
|
|
228
|
+
"+",
|
|
229
|
+
limit,
|
|
230
|
+
)
|
|
231
|
+
message_ids = [self._pending_id(item) for item in pending]
|
|
232
|
+
if not message_ids:
|
|
233
|
+
return []
|
|
234
|
+
claimed = self._execute(
|
|
235
|
+
"redis.xclaim",
|
|
236
|
+
self.redis.xclaim,
|
|
237
|
+
self.stream_key,
|
|
238
|
+
self.config.consumer_group,
|
|
239
|
+
self._consumer_name(),
|
|
240
|
+
min_idle_ms,
|
|
241
|
+
message_ids,
|
|
242
|
+
)
|
|
243
|
+
return [
|
|
244
|
+
self._decode_stream_entry(str(raw_id), fields)
|
|
245
|
+
for raw_id, fields in claimed
|
|
246
|
+
]
|
|
247
|
+
response = self._execute(
|
|
248
|
+
"redis.xautoclaim",
|
|
249
|
+
self.redis.xautoclaim,
|
|
250
|
+
self.stream_key,
|
|
251
|
+
self.config.consumer_group,
|
|
252
|
+
self._consumer_name(),
|
|
253
|
+
min_idle_ms,
|
|
254
|
+
"0-0",
|
|
255
|
+
limit,
|
|
256
|
+
)
|
|
257
|
+
return self._parse_autoclaim_response(response)
|
|
258
|
+
|
|
259
|
+
def dead_letters(self, *, limit: int = 100) -> list[Message]:
|
|
260
|
+
response = self._execute(
|
|
261
|
+
"redis.xreadgroup",
|
|
262
|
+
self.redis.xreadgroup,
|
|
263
|
+
self.config.consumer_group,
|
|
264
|
+
self._consumer_name(),
|
|
265
|
+
{self.dead_key: ">"},
|
|
266
|
+
limit,
|
|
267
|
+
None,
|
|
268
|
+
)
|
|
269
|
+
return self._parse_read_response(response)
|
|
270
|
+
|
|
271
|
+
def requeue_dead(self, message: Message) -> None:
|
|
272
|
+
self.publish(message.payload, headers=message.headers, message_id=message.id)
|
|
273
|
+
if message.raw_id:
|
|
274
|
+
self._execute(
|
|
275
|
+
"redis.xack",
|
|
276
|
+
self.redis.xack,
|
|
277
|
+
self.dead_key,
|
|
278
|
+
self.config.consumer_group,
|
|
279
|
+
message.raw_id,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _publish_message(self, message: Message) -> str:
|
|
283
|
+
raw_id = self._execute(
|
|
284
|
+
"redis.xadd",
|
|
285
|
+
self.redis.xadd,
|
|
286
|
+
self.stream_key,
|
|
287
|
+
{"payload": self._encode(message)},
|
|
288
|
+
)
|
|
289
|
+
published = message.with_backend(self.backend_name, raw_id=str(raw_id))
|
|
290
|
+
self._emit(
|
|
291
|
+
MonitoringEventType.MESSAGE_PUBLISHED,
|
|
292
|
+
published,
|
|
293
|
+
attributes={"key": self.stream_key},
|
|
294
|
+
)
|
|
295
|
+
return str(raw_id)
|
|
296
|
+
|
|
297
|
+
def _ensure_group(self) -> None:
|
|
298
|
+
try:
|
|
299
|
+
self.redis.xgroup_create(
|
|
300
|
+
self.stream_key,
|
|
301
|
+
self.config.consumer_group,
|
|
302
|
+
id="0",
|
|
303
|
+
mkstream=True,
|
|
304
|
+
)
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
if "BUSYGROUP" in str(exc):
|
|
307
|
+
return
|
|
308
|
+
self._emit_backend_error("redis.xgroup_create", str(exc))
|
|
309
|
+
raise BackendUnavailableError(
|
|
310
|
+
"Redis Streams group initialization failed",
|
|
311
|
+
action="redis.xgroup_create",
|
|
312
|
+
queue=self.config.queue,
|
|
313
|
+
) from exc
|
|
314
|
+
|
|
315
|
+
def _move_to_dead(self, message: Message) -> None:
|
|
316
|
+
self._execute(
|
|
317
|
+
"redis.xadd",
|
|
318
|
+
self.redis.xadd,
|
|
319
|
+
self.dead_key,
|
|
320
|
+
{"payload": self._encode(message)},
|
|
321
|
+
)
|
|
322
|
+
self.ack(message)
|
|
323
|
+
|
|
324
|
+
def _parse_read_response(self, response: list[Any]) -> list[Message]:
|
|
325
|
+
messages: list[Message] = []
|
|
326
|
+
for _stream, entries in response or []:
|
|
327
|
+
for raw_id, fields in entries:
|
|
328
|
+
messages.append(self._decode_stream_entry(str(raw_id), fields))
|
|
329
|
+
return messages
|
|
330
|
+
|
|
331
|
+
def _parse_autoclaim_response(self, response: Any) -> list[Message]:
|
|
332
|
+
if not response:
|
|
333
|
+
return []
|
|
334
|
+
entries = response[1] if len(response) > 1 else []
|
|
335
|
+
return [
|
|
336
|
+
self._decode_stream_entry(str(raw_id), fields)
|
|
337
|
+
for raw_id, fields in entries
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
def _decode_stream_entry(self, raw_id: str, fields: dict[Any, Any]) -> Message:
|
|
341
|
+
payload = fields.get("payload") or fields.get(b"payload")
|
|
342
|
+
if payload is None:
|
|
343
|
+
raise BackendUnavailableError(
|
|
344
|
+
"stream entry is missing payload field",
|
|
345
|
+
action="message.decode",
|
|
346
|
+
queue=self.config.queue,
|
|
347
|
+
details={"raw_id": raw_id},
|
|
348
|
+
)
|
|
349
|
+
return self._decode(payload).with_backend(self.backend_name, raw_id=raw_id)
|
|
350
|
+
|
|
351
|
+
def _consumer_name(self) -> str:
|
|
352
|
+
return self.config.consumer_name or "redqueue-consumer"
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def _pending_id(item: Any) -> str:
|
|
356
|
+
if isinstance(item, dict):
|
|
357
|
+
value = item.get("message_id") or item.get("message-id") or item.get("id")
|
|
358
|
+
return str(value)
|
|
359
|
+
return str(item[0])
|
|
360
|
+
|
|
361
|
+
def _execute(self, action: str, func: Any, *args: Any) -> Any:
|
|
362
|
+
try:
|
|
363
|
+
return func(*args)
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
self._emit_backend_error(action, str(exc))
|
|
366
|
+
raise BackendUnavailableError(
|
|
367
|
+
"Redis Streams backend command failed",
|
|
368
|
+
action=action,
|
|
369
|
+
queue=self.config.queue,
|
|
370
|
+
) from exc
|
redqueue/client.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Synchronous RedQueue client."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from redis import Redis
|
|
11
|
+
|
|
12
|
+
from redqueue.backends import DelayBackend, ListBackend, StreamBackend
|
|
13
|
+
from redqueue.compat import RedisCapabilities, RedisInfoClient, detect_capabilities
|
|
14
|
+
from redqueue.config import BackendType, QueueConfig
|
|
15
|
+
from redqueue.message import Message
|
|
16
|
+
from redqueue.monitoring import MonitoringEvent, MonitoringEventType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueueClient:
|
|
20
|
+
"""Synchronous queue client."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
config: QueueConfig,
|
|
25
|
+
*,
|
|
26
|
+
redis: Any | None = None,
|
|
27
|
+
capabilities: RedisCapabilities | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.config = config
|
|
30
|
+
self.redis = redis
|
|
31
|
+
self.capabilities = capabilities
|
|
32
|
+
self.backend = self._create_backend()
|
|
33
|
+
self.delay_backend = self._create_delay_backend()
|
|
34
|
+
self.config.monitoring.emit(
|
|
35
|
+
MonitoringEvent(
|
|
36
|
+
type=MonitoringEventType.CLIENT_CREATED,
|
|
37
|
+
queue=config.queue,
|
|
38
|
+
backend=config.backend_type.value,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_url(
|
|
44
|
+
cls,
|
|
45
|
+
url: str,
|
|
46
|
+
*,
|
|
47
|
+
queue: str,
|
|
48
|
+
backend: str | BackendType = BackendType.LIST,
|
|
49
|
+
**options: Any,
|
|
50
|
+
) -> QueueClient:
|
|
51
|
+
redis = options.pop("redis", None) or Redis.from_url(url)
|
|
52
|
+
capabilities = options.pop("capabilities", None) or detect_capabilities(
|
|
53
|
+
cast(RedisInfoClient, redis)
|
|
54
|
+
)
|
|
55
|
+
config = QueueConfig(queue=queue, backend=backend, **options)
|
|
56
|
+
return cls(config=config, redis=redis, capabilities=capabilities)
|
|
57
|
+
|
|
58
|
+
def publish(
|
|
59
|
+
self,
|
|
60
|
+
payload: Any,
|
|
61
|
+
*,
|
|
62
|
+
delay: float | None = None,
|
|
63
|
+
headers: dict[str, Any] | None = None,
|
|
64
|
+
message_id: str | None = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
if delay is not None:
|
|
67
|
+
return self.delay(payload, delay_seconds=delay, headers=headers)
|
|
68
|
+
return self.backend.publish(
|
|
69
|
+
payload,
|
|
70
|
+
headers=headers,
|
|
71
|
+
message_id=message_id,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def consume(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
timeout: float | None = None,
|
|
78
|
+
batch_size: int = 1,
|
|
79
|
+
) -> Message | list[Message] | None:
|
|
80
|
+
return self.backend.consume(timeout=timeout, batch_size=batch_size)
|
|
81
|
+
|
|
82
|
+
def ack(self, message: Message) -> None:
|
|
83
|
+
self.backend.ack(message)
|
|
84
|
+
|
|
85
|
+
def nack(self, message: Message, *, requeue: bool = True) -> None:
|
|
86
|
+
self.backend.nack(message, requeue=requeue)
|
|
87
|
+
|
|
88
|
+
def retry(
|
|
89
|
+
self,
|
|
90
|
+
message: Message,
|
|
91
|
+
*,
|
|
92
|
+
delay: float | None = None,
|
|
93
|
+
reason: str | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self.backend.retry(message, delay=delay, reason=reason)
|
|
96
|
+
|
|
97
|
+
def delay(
|
|
98
|
+
self,
|
|
99
|
+
payload: Any,
|
|
100
|
+
*,
|
|
101
|
+
delay_seconds: float | None = None,
|
|
102
|
+
run_at: float | None = None,
|
|
103
|
+
headers: dict[str, Any] | None = None,
|
|
104
|
+
) -> str:
|
|
105
|
+
message_id = self.delay_backend.delay(
|
|
106
|
+
payload,
|
|
107
|
+
delay_seconds=delay_seconds,
|
|
108
|
+
run_at=run_at,
|
|
109
|
+
headers=headers,
|
|
110
|
+
)
|
|
111
|
+
self.config.monitoring.emit(
|
|
112
|
+
MonitoringEvent(
|
|
113
|
+
type=MonitoringEventType.DELAY_SCHEDULED,
|
|
114
|
+
queue=self.config.queue,
|
|
115
|
+
message_id=message_id,
|
|
116
|
+
backend=self.config.backend_type.value,
|
|
117
|
+
attributes={"delay_seconds": delay_seconds, "run_at": run_at},
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return message_id
|
|
121
|
+
|
|
122
|
+
def schedule_due(self, *, limit: int = 100, now: float | None = None) -> int:
|
|
123
|
+
return self.delay_backend.schedule_due(limit=limit, now=now)
|
|
124
|
+
|
|
125
|
+
def recover_stale(self, *, min_idle_ms: int | None = None, limit: int = 100) -> int:
|
|
126
|
+
if isinstance(self.backend, StreamBackend):
|
|
127
|
+
idle = min_idle_ms or int(self.config.visibility_timeout_seconds * 1000)
|
|
128
|
+
return len(self.backend.recover_pending(min_idle_ms=idle, limit=limit))
|
|
129
|
+
return self.backend.recover_stale(limit=limit)
|
|
130
|
+
|
|
131
|
+
def dead_letters(self, *, limit: int = 100) -> list[Message]:
|
|
132
|
+
return self.backend.dead_letters(limit=limit)
|
|
133
|
+
|
|
134
|
+
def requeue_dead(self, message: Message) -> None:
|
|
135
|
+
self.backend.requeue_dead(message)
|
|
136
|
+
|
|
137
|
+
def close(self) -> None:
|
|
138
|
+
close = getattr(self.redis, "close", None)
|
|
139
|
+
if close is not None:
|
|
140
|
+
close()
|
|
141
|
+
|
|
142
|
+
def _create_backend(self) -> ListBackend | StreamBackend:
|
|
143
|
+
if self.config.backend_type is BackendType.LIST:
|
|
144
|
+
if self.redis is None:
|
|
145
|
+
raise TypeError("redis client is required for List backend")
|
|
146
|
+
capabilities = self.capabilities or detect_capabilities(
|
|
147
|
+
cast(RedisInfoClient, self.redis)
|
|
148
|
+
)
|
|
149
|
+
return ListBackend(self.redis, self.config, capabilities)
|
|
150
|
+
if self.config.backend_type is BackendType.STREAM:
|
|
151
|
+
if self.redis is None:
|
|
152
|
+
raise TypeError("redis client is required for Streams backend")
|
|
153
|
+
capabilities = self.capabilities or detect_capabilities(
|
|
154
|
+
cast(RedisInfoClient, self.redis)
|
|
155
|
+
)
|
|
156
|
+
return StreamBackend(self.redis, self.config, capabilities)
|
|
157
|
+
raise NotImplementedError(
|
|
158
|
+
f"backend {self.config.backend_type.value!r} is not implemented"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _create_delay_backend(self) -> DelayBackend:
|
|
162
|
+
if self.redis is None:
|
|
163
|
+
raise TypeError("redis client is required for delayed tasks")
|
|
164
|
+
capabilities = self.capabilities or detect_capabilities(
|
|
165
|
+
cast(RedisInfoClient, self.redis)
|
|
166
|
+
)
|
|
167
|
+
capabilities.require_delay_sorted_set()
|
|
168
|
+
return DelayBackend(self.redis, self.config, self.backend)
|