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
redqueue/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Redis-backed message queue library."""
|
|
5
|
+
|
|
6
|
+
from redqueue._version import __version__
|
|
7
|
+
from redqueue.async_client import AsyncQueueClient
|
|
8
|
+
from redqueue.client import QueueClient
|
|
9
|
+
from redqueue.compat import (
|
|
10
|
+
RedisCapabilities,
|
|
11
|
+
RedisVersion,
|
|
12
|
+
detect_capabilities,
|
|
13
|
+
detect_capabilities_async,
|
|
14
|
+
extract_redis_version,
|
|
15
|
+
)
|
|
16
|
+
from redqueue.config import BackendType, QueueConfig, RetryConfig
|
|
17
|
+
from redqueue.exceptions import (
|
|
18
|
+
AckError,
|
|
19
|
+
BackendUnavailableError,
|
|
20
|
+
ErrorContext,
|
|
21
|
+
MessageDecodeError,
|
|
22
|
+
MessageEncodeError,
|
|
23
|
+
QueueConfigError,
|
|
24
|
+
RedisCompatibilityError,
|
|
25
|
+
RedQueueError,
|
|
26
|
+
RetryExceededError,
|
|
27
|
+
)
|
|
28
|
+
from redqueue.message import Message, new_message_id
|
|
29
|
+
from redqueue.monitoring import (
|
|
30
|
+
CompositeMonitoringHook,
|
|
31
|
+
InMemoryMonitoringHook,
|
|
32
|
+
MonitoringEvent,
|
|
33
|
+
MonitoringEventType,
|
|
34
|
+
MonitoringHook,
|
|
35
|
+
NoopMonitoringHook,
|
|
36
|
+
SafeMonitoringHook,
|
|
37
|
+
)
|
|
38
|
+
from redqueue.serialization import JsonSerializer, Serializer
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"AckError",
|
|
43
|
+
"AsyncQueueClient",
|
|
44
|
+
"BackendType",
|
|
45
|
+
"BackendUnavailableError",
|
|
46
|
+
"ErrorContext",
|
|
47
|
+
"CompositeMonitoringHook",
|
|
48
|
+
"InMemoryMonitoringHook",
|
|
49
|
+
"JsonSerializer",
|
|
50
|
+
"Message",
|
|
51
|
+
"MessageDecodeError",
|
|
52
|
+
"MessageEncodeError",
|
|
53
|
+
"MonitoringEvent",
|
|
54
|
+
"MonitoringEventType",
|
|
55
|
+
"MonitoringHook",
|
|
56
|
+
"NoopMonitoringHook",
|
|
57
|
+
"QueueClient",
|
|
58
|
+
"QueueConfig",
|
|
59
|
+
"QueueConfigError",
|
|
60
|
+
"RedisCapabilities",
|
|
61
|
+
"RedQueueError",
|
|
62
|
+
"RedisCompatibilityError",
|
|
63
|
+
"RedisVersion",
|
|
64
|
+
"RetryConfig",
|
|
65
|
+
"RetryExceededError",
|
|
66
|
+
"SafeMonitoringHook",
|
|
67
|
+
"Serializer",
|
|
68
|
+
"detect_capabilities",
|
|
69
|
+
"detect_capabilities_async",
|
|
70
|
+
"extract_redis_version",
|
|
71
|
+
"new_message_id",
|
|
72
|
+
]
|
redqueue/_version.py
ADDED
redqueue/async_client.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Asynchronous RedQueue client."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from redis.asyncio import Redis
|
|
11
|
+
|
|
12
|
+
from redqueue.backends import AsyncDelayBackend, AsyncListBackend, AsyncStreamBackend
|
|
13
|
+
from redqueue.compat import (
|
|
14
|
+
AsyncRedisInfoClient,
|
|
15
|
+
RedisCapabilities,
|
|
16
|
+
detect_capabilities_async,
|
|
17
|
+
)
|
|
18
|
+
from redqueue.config import BackendType, QueueConfig
|
|
19
|
+
from redqueue.message import Message
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncQueueClient:
|
|
23
|
+
"""Asynchronous queue client with API parity to QueueClient."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
config: QueueConfig,
|
|
28
|
+
*,
|
|
29
|
+
redis: Any | None = None,
|
|
30
|
+
capabilities: RedisCapabilities | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.config = config
|
|
33
|
+
self.redis = redis
|
|
34
|
+
self.capabilities = capabilities
|
|
35
|
+
self.backend: AsyncListBackend | AsyncStreamBackend | None = None
|
|
36
|
+
self.delay_backend: AsyncDelayBackend | None = None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
async def from_url(
|
|
40
|
+
cls,
|
|
41
|
+
url: str,
|
|
42
|
+
*,
|
|
43
|
+
queue: str,
|
|
44
|
+
backend: str | BackendType = BackendType.LIST,
|
|
45
|
+
**options: Any,
|
|
46
|
+
) -> AsyncQueueClient:
|
|
47
|
+
redis = options.pop("redis", None) or Redis.from_url(url)
|
|
48
|
+
capabilities = options.pop(
|
|
49
|
+
"capabilities",
|
|
50
|
+
None,
|
|
51
|
+
) or await detect_capabilities_async(cast(AsyncRedisInfoClient, redis))
|
|
52
|
+
config = QueueConfig(queue=queue, backend=backend, **options)
|
|
53
|
+
client = cls(config=config, redis=redis, capabilities=capabilities)
|
|
54
|
+
await client._ensure_backend()
|
|
55
|
+
return client
|
|
56
|
+
|
|
57
|
+
async def publish(
|
|
58
|
+
self,
|
|
59
|
+
payload: Any,
|
|
60
|
+
*,
|
|
61
|
+
delay: float | None = None,
|
|
62
|
+
headers: dict[str, Any] | None = None,
|
|
63
|
+
message_id: str | None = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
if delay is not None:
|
|
66
|
+
return await self.delay(payload, delay_seconds=delay, headers=headers)
|
|
67
|
+
backend = await self._ensure_backend()
|
|
68
|
+
return await backend.publish(
|
|
69
|
+
payload,
|
|
70
|
+
headers=headers,
|
|
71
|
+
message_id=message_id,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def consume(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
timeout: float | None = None,
|
|
78
|
+
batch_size: int = 1,
|
|
79
|
+
) -> Message | list[Message] | None:
|
|
80
|
+
backend = await self._ensure_backend()
|
|
81
|
+
return await backend.consume(
|
|
82
|
+
timeout=timeout,
|
|
83
|
+
batch_size=batch_size,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def ack(self, message: Message) -> None:
|
|
87
|
+
await (await self._ensure_backend()).ack(message)
|
|
88
|
+
|
|
89
|
+
async def nack(self, message: Message, *, requeue: bool = True) -> None:
|
|
90
|
+
await (await self._ensure_backend()).nack(message, requeue=requeue)
|
|
91
|
+
|
|
92
|
+
async def retry(
|
|
93
|
+
self,
|
|
94
|
+
message: Message,
|
|
95
|
+
*,
|
|
96
|
+
delay: float | None = None,
|
|
97
|
+
reason: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
await (await self._ensure_backend()).retry(message, delay=delay, reason=reason)
|
|
100
|
+
|
|
101
|
+
async def delay(
|
|
102
|
+
self,
|
|
103
|
+
payload: Any,
|
|
104
|
+
*,
|
|
105
|
+
delay_seconds: float | None = None,
|
|
106
|
+
run_at: float | None = None,
|
|
107
|
+
headers: dict[str, Any] | None = None,
|
|
108
|
+
) -> str:
|
|
109
|
+
delay_backend = await self._ensure_delay_backend()
|
|
110
|
+
message_id = await delay_backend.delay(
|
|
111
|
+
payload,
|
|
112
|
+
delay_seconds=delay_seconds,
|
|
113
|
+
run_at=run_at,
|
|
114
|
+
headers=headers,
|
|
115
|
+
)
|
|
116
|
+
return message_id
|
|
117
|
+
|
|
118
|
+
async def schedule_due(self, *, limit: int = 100, now: float | None = None) -> int:
|
|
119
|
+
return await (await self._ensure_delay_backend()).schedule_due(
|
|
120
|
+
limit=limit,
|
|
121
|
+
now=now,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def recover_stale(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
min_idle_ms: int | None = None,
|
|
128
|
+
limit: int = 100,
|
|
129
|
+
) -> int:
|
|
130
|
+
backend = await self._ensure_backend()
|
|
131
|
+
if isinstance(backend, AsyncStreamBackend):
|
|
132
|
+
idle = min_idle_ms or int(self.config.visibility_timeout_seconds * 1000)
|
|
133
|
+
return len(await backend.recover_pending(min_idle_ms=idle, limit=limit))
|
|
134
|
+
return await backend.recover_stale(limit=limit)
|
|
135
|
+
|
|
136
|
+
async def dead_letters(self, *, limit: int = 100) -> list[Message]:
|
|
137
|
+
return await (await self._ensure_backend()).dead_letters(limit=limit)
|
|
138
|
+
|
|
139
|
+
async def requeue_dead(self, message: Message) -> None:
|
|
140
|
+
await (await self._ensure_backend()).requeue_dead(message)
|
|
141
|
+
|
|
142
|
+
async def close(self) -> None:
|
|
143
|
+
close = getattr(self.redis, "aclose", None) or getattr(
|
|
144
|
+
self.redis,
|
|
145
|
+
"close",
|
|
146
|
+
None,
|
|
147
|
+
)
|
|
148
|
+
if close is not None:
|
|
149
|
+
result = close()
|
|
150
|
+
if hasattr(result, "__await__"):
|
|
151
|
+
await result
|
|
152
|
+
|
|
153
|
+
async def _ensure_backend(self) -> AsyncListBackend | AsyncStreamBackend:
|
|
154
|
+
if self.backend is not None:
|
|
155
|
+
return self.backend
|
|
156
|
+
if self.config.backend_type is BackendType.LIST:
|
|
157
|
+
if self.redis is None:
|
|
158
|
+
raise TypeError("redis client is required for async List backend")
|
|
159
|
+
capabilities = self.capabilities
|
|
160
|
+
if capabilities is None:
|
|
161
|
+
raise TypeError("Redis capabilities are required before backend use")
|
|
162
|
+
self.backend = AsyncListBackend(self.redis, self.config, capabilities)
|
|
163
|
+
return self.backend
|
|
164
|
+
if self.config.backend_type is BackendType.STREAM:
|
|
165
|
+
if self.redis is None:
|
|
166
|
+
raise TypeError("redis client is required for async Streams backend")
|
|
167
|
+
capabilities = self.capabilities
|
|
168
|
+
if capabilities is None:
|
|
169
|
+
raise TypeError("Redis capabilities are required before backend use")
|
|
170
|
+
self.backend = await AsyncStreamBackend.create(
|
|
171
|
+
self.redis,
|
|
172
|
+
self.config,
|
|
173
|
+
capabilities,
|
|
174
|
+
)
|
|
175
|
+
return self.backend
|
|
176
|
+
raise NotImplementedError(
|
|
177
|
+
f"backend {self.config.backend_type.value!r} is not implemented"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def _ensure_delay_backend(self) -> AsyncDelayBackend:
|
|
181
|
+
if self.delay_backend is not None:
|
|
182
|
+
return self.delay_backend
|
|
183
|
+
if self.redis is None:
|
|
184
|
+
raise TypeError("redis client is required for async delayed tasks")
|
|
185
|
+
capabilities = self.capabilities
|
|
186
|
+
if capabilities is None:
|
|
187
|
+
raise TypeError("Redis capabilities are required before delay backend use")
|
|
188
|
+
capabilities.require_delay_sorted_set()
|
|
189
|
+
self.delay_backend = AsyncDelayBackend(
|
|
190
|
+
self.redis,
|
|
191
|
+
self.config,
|
|
192
|
+
await self._ensure_backend(),
|
|
193
|
+
)
|
|
194
|
+
return self.delay_backend
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Backend implementations for RedQueue."""
|
|
5
|
+
|
|
6
|
+
from redqueue.backends.async_delay import AsyncDelayBackend
|
|
7
|
+
from redqueue.backends.async_list import AsyncListBackend
|
|
8
|
+
from redqueue.backends.async_stream import AsyncStreamBackend
|
|
9
|
+
from redqueue.backends.delay import DelayBackend
|
|
10
|
+
from redqueue.backends.list import ListBackend
|
|
11
|
+
from redqueue.backends.stream import StreamBackend
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AsyncDelayBackend",
|
|
15
|
+
"AsyncListBackend",
|
|
16
|
+
"AsyncStreamBackend",
|
|
17
|
+
"DelayBackend",
|
|
18
|
+
"ListBackend",
|
|
19
|
+
"StreamBackend",
|
|
20
|
+
]
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Author: SpringMirror-pear
|
|
3
|
+
|
|
4
|
+
"""Asynchronous delayed task backend based on Redis Sorted Set."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
from redqueue.config import QueueConfig
|
|
12
|
+
from redqueue.exceptions import BackendUnavailableError, QueueConfigError
|
|
13
|
+
from redqueue.message import Message, new_message_id
|
|
14
|
+
from redqueue.monitoring import MonitoringEvent, MonitoringEventType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncDelayRedis(Protocol):
|
|
18
|
+
"""Redis command subset required by the async delayed task backend."""
|
|
19
|
+
|
|
20
|
+
async def set(self, name: str, value: bytes) -> bool: ...
|
|
21
|
+
|
|
22
|
+
async def get(self, name: str) -> bytes | None: ...
|
|
23
|
+
|
|
24
|
+
async def delete(self, *names: str) -> int: ...
|
|
25
|
+
|
|
26
|
+
async def zadd(self, name: str, mapping: dict[str, float]) -> int: ...
|
|
27
|
+
|
|
28
|
+
async def zrangebyscore(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
min: float | str,
|
|
32
|
+
max: float | str,
|
|
33
|
+
start: int | None = None,
|
|
34
|
+
num: int | None = None,
|
|
35
|
+
) -> list[str | bytes]: ...
|
|
36
|
+
|
|
37
|
+
async def zrem(self, name: str, *values: str) -> int: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AsyncDelayBackend:
|
|
41
|
+
"""Async delayed task scheduler implemented with Redis Sorted Set."""
|
|
42
|
+
|
|
43
|
+
backend_name = "delay"
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
redis: AsyncDelayRedis,
|
|
48
|
+
config: QueueConfig,
|
|
49
|
+
publisher: Any,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.redis = redis
|
|
52
|
+
self.config = config
|
|
53
|
+
self.publisher = publisher
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def delayed_key(self) -> str:
|
|
57
|
+
return self.config.key("delayed")
|
|
58
|
+
|
|
59
|
+
def payload_key(self, message_id: str) -> str:
|
|
60
|
+
return self.config.key(f"payload:{message_id}")
|
|
61
|
+
|
|
62
|
+
async def delay(
|
|
63
|
+
self,
|
|
64
|
+
payload: Any,
|
|
65
|
+
*,
|
|
66
|
+
delay_seconds: float | None = None,
|
|
67
|
+
run_at: float | None = None,
|
|
68
|
+
headers: dict[str, Any] | None = None,
|
|
69
|
+
message_id: str | None = None,
|
|
70
|
+
) -> str:
|
|
71
|
+
available_at = self._available_at(delay_seconds=delay_seconds, run_at=run_at)
|
|
72
|
+
message = Message(
|
|
73
|
+
id=message_id or new_message_id(),
|
|
74
|
+
queue=self.config.queue,
|
|
75
|
+
payload=payload,
|
|
76
|
+
headers=headers or {},
|
|
77
|
+
available_at=available_at,
|
|
78
|
+
backend=self.backend_name,
|
|
79
|
+
)
|
|
80
|
+
await self._execute(
|
|
81
|
+
"redis.set",
|
|
82
|
+
self.redis.set,
|
|
83
|
+
self.payload_key(message.id),
|
|
84
|
+
self._encode(message),
|
|
85
|
+
)
|
|
86
|
+
await self._execute(
|
|
87
|
+
"redis.zadd",
|
|
88
|
+
self.redis.zadd,
|
|
89
|
+
self.delayed_key,
|
|
90
|
+
{message.id: available_at},
|
|
91
|
+
)
|
|
92
|
+
self._emit(MonitoringEventType.DELAY_SCHEDULED, message)
|
|
93
|
+
return message.id
|
|
94
|
+
|
|
95
|
+
async def schedule_due(self, *, limit: int = 100, now: float | None = None) -> int:
|
|
96
|
+
now_value = time() if now is None else now
|
|
97
|
+
due_ids = await self._execute(
|
|
98
|
+
"redis.zrangebyscore",
|
|
99
|
+
self.redis.zrangebyscore,
|
|
100
|
+
self.delayed_key,
|
|
101
|
+
"-inf",
|
|
102
|
+
now_value,
|
|
103
|
+
0,
|
|
104
|
+
limit,
|
|
105
|
+
)
|
|
106
|
+
released = 0
|
|
107
|
+
for raw_message_id in due_ids:
|
|
108
|
+
message_id = self._to_text(raw_message_id)
|
|
109
|
+
removed = await self._execute(
|
|
110
|
+
"redis.zrem",
|
|
111
|
+
self.redis.zrem,
|
|
112
|
+
self.delayed_key,
|
|
113
|
+
message_id,
|
|
114
|
+
)
|
|
115
|
+
if removed < 1:
|
|
116
|
+
continue
|
|
117
|
+
message = await self._load_message(message_id)
|
|
118
|
+
try:
|
|
119
|
+
await self.publisher.publish(
|
|
120
|
+
message.payload,
|
|
121
|
+
headers=message.headers,
|
|
122
|
+
message_id=message.id,
|
|
123
|
+
)
|
|
124
|
+
except Exception:
|
|
125
|
+
await self._execute(
|
|
126
|
+
"redis.zadd",
|
|
127
|
+
self.redis.zadd,
|
|
128
|
+
self.delayed_key,
|
|
129
|
+
{message_id: message.available_at or now_value},
|
|
130
|
+
)
|
|
131
|
+
raise
|
|
132
|
+
await self._execute(
|
|
133
|
+
"redis.delete",
|
|
134
|
+
self.redis.delete,
|
|
135
|
+
self.payload_key(message_id),
|
|
136
|
+
)
|
|
137
|
+
self._emit(MonitoringEventType.DELAY_RELEASED, message)
|
|
138
|
+
released += 1
|
|
139
|
+
return released
|
|
140
|
+
|
|
141
|
+
async def _load_message(self, message_id: str) -> Message:
|
|
142
|
+
payload = await self._execute(
|
|
143
|
+
"redis.get",
|
|
144
|
+
self.redis.get,
|
|
145
|
+
self.payload_key(message_id),
|
|
146
|
+
)
|
|
147
|
+
if payload is None:
|
|
148
|
+
raise BackendUnavailableError(
|
|
149
|
+
"delayed payload is missing",
|
|
150
|
+
action="delay.load",
|
|
151
|
+
queue=self.config.queue,
|
|
152
|
+
details={"message_id": message_id},
|
|
153
|
+
)
|
|
154
|
+
return self._decode(payload)
|
|
155
|
+
|
|
156
|
+
def _available_at(
|
|
157
|
+
self,
|
|
158
|
+
*,
|
|
159
|
+
delay_seconds: float | None,
|
|
160
|
+
run_at: float | None,
|
|
161
|
+
) -> float:
|
|
162
|
+
if delay_seconds is not None and run_at is not None:
|
|
163
|
+
raise QueueConfigError("delay_seconds and run_at cannot both be set")
|
|
164
|
+
if delay_seconds is not None:
|
|
165
|
+
if delay_seconds < 0:
|
|
166
|
+
raise QueueConfigError(
|
|
167
|
+
"delay_seconds must be greater than or equal to 0"
|
|
168
|
+
)
|
|
169
|
+
return time() + delay_seconds
|
|
170
|
+
if run_at is not None:
|
|
171
|
+
if run_at < 0:
|
|
172
|
+
raise QueueConfigError("run_at must be greater than or equal to 0")
|
|
173
|
+
return run_at
|
|
174
|
+
return time()
|
|
175
|
+
|
|
176
|
+
def _encode(self, message: Message) -> bytes:
|
|
177
|
+
envelope = {
|
|
178
|
+
"id": message.id,
|
|
179
|
+
"queue": message.queue,
|
|
180
|
+
"payload": message.payload,
|
|
181
|
+
"headers": message.headers,
|
|
182
|
+
"attempts": message.attempts,
|
|
183
|
+
"created_at": message.created_at,
|
|
184
|
+
"available_at": message.available_at,
|
|
185
|
+
"backend": self.backend_name,
|
|
186
|
+
"raw_id": message.raw_id,
|
|
187
|
+
}
|
|
188
|
+
return self.config.serializer.encode(envelope, queue=self.config.queue)
|
|
189
|
+
|
|
190
|
+
def _decode(self, payload: bytes) -> Message:
|
|
191
|
+
envelope = self.config.serializer.decode(payload, queue=self.config.queue)
|
|
192
|
+
if not isinstance(envelope, dict):
|
|
193
|
+
raise BackendUnavailableError(
|
|
194
|
+
"decoded delayed message envelope must be a mapping",
|
|
195
|
+
action="delay.decode",
|
|
196
|
+
queue=self.config.queue,
|
|
197
|
+
details={"payload_type": type(envelope).__name__},
|
|
198
|
+
)
|
|
199
|
+
return Message(
|
|
200
|
+
id=str(envelope["id"]),
|
|
201
|
+
queue=str(envelope["queue"]),
|
|
202
|
+
payload=envelope["payload"],
|
|
203
|
+
headers=dict(envelope.get("headers") or {}),
|
|
204
|
+
attempts=int(envelope.get("attempts") or 0),
|
|
205
|
+
created_at=float(envelope["created_at"]),
|
|
206
|
+
available_at=envelope.get("available_at"),
|
|
207
|
+
backend=self.backend_name,
|
|
208
|
+
raw_id=envelope.get("raw_id"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def _execute(self, action: str, func: Any, *args: Any) -> Any:
|
|
212
|
+
try:
|
|
213
|
+
return await func(*args)
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
self.config.monitoring.emit(
|
|
216
|
+
MonitoringEvent(
|
|
217
|
+
type=MonitoringEventType.BACKEND_ERROR,
|
|
218
|
+
queue=self.config.queue,
|
|
219
|
+
backend=self.backend_name,
|
|
220
|
+
error=str(exc),
|
|
221
|
+
attributes={"action": action},
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
raise BackendUnavailableError(
|
|
225
|
+
"Redis async delayed task backend command failed",
|
|
226
|
+
action=action,
|
|
227
|
+
queue=self.config.queue,
|
|
228
|
+
) from exc
|
|
229
|
+
|
|
230
|
+
def _emit(self, event_type: MonitoringEventType, message: Message) -> None:
|
|
231
|
+
self.config.monitoring.emit(
|
|
232
|
+
MonitoringEvent(
|
|
233
|
+
type=event_type,
|
|
234
|
+
queue=self.config.queue,
|
|
235
|
+
message_id=message.id,
|
|
236
|
+
backend=self.backend_name,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _to_text(value: str | bytes) -> str:
|
|
242
|
+
return value.decode() if isinstance(value, bytes) else value
|