aury-boot 0.0.43__py3-none-any.whl → 0.0.45__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.
- aury/boot/_version.py +2 -2
- aury/boot/infrastructure/cache/redis.py +22 -44
- aury/boot/infrastructure/channel/__init__.py +2 -2
- aury/boot/infrastructure/channel/backends/__init__.py +1 -2
- aury/boot/infrastructure/channel/base.py +1 -3
- aury/boot/infrastructure/channel/manager.py +16 -8
- aury/boot/infrastructure/clients/redis/manager.py +18 -44
- aury/boot/infrastructure/mq/backends/redis_stream.py +6 -17
- {aury_boot-0.0.43.dist-info → aury_boot-0.0.45.dist-info}/METADATA +1 -5
- {aury_boot-0.0.43.dist-info → aury_boot-0.0.45.dist-info}/RECORD +12 -14
- aury/boot/infrastructure/channel/backends/redis_cluster.py +0 -136
- aury/boot/infrastructure/channel/backends/redis_cluster_channel.py +0 -139
- {aury_boot-0.0.43.dist-info → aury_boot-0.0.45.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.43.dist-info → aury_boot-0.0.45.dist-info}/entry_points.txt +0 -0
aury/boot/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.45'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 45)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -96,58 +96,33 @@ class RedisCache(ICache):
|
|
|
96
96
|
self._is_cluster = False
|
|
97
97
|
|
|
98
98
|
async def _init_cluster(self) -> None:
|
|
99
|
-
"""初始化 Redis Cluster(使用
|
|
99
|
+
"""初始化 Redis Cluster(使用 redis-py)。
|
|
100
100
|
|
|
101
101
|
支持 URL 格式:
|
|
102
102
|
- redis-cluster://password@host:port (密码在用户名位置)
|
|
103
103
|
- redis-cluster://:password@host:port (标准格式)
|
|
104
104
|
- redis-cluster://username:password@host:port (ACL 模式)
|
|
105
105
|
"""
|
|
106
|
-
|
|
107
|
-
from coredis import RedisCluster
|
|
108
|
-
from coredis.retry import ConstantRetryPolicy
|
|
109
|
-
from coredis.exceptions import ConnectionError as CoredisConnectionError
|
|
110
|
-
except ImportError as exc:
|
|
111
|
-
raise ImportError(
|
|
112
|
-
"Redis Cluster 需要安装 coredis: pip install coredis"
|
|
113
|
-
) from exc
|
|
114
|
-
|
|
115
|
-
# 解析 URL
|
|
116
|
-
parsed_url = self._url.replace("redis-cluster://", "redis://")
|
|
117
|
-
parsed = urlparse(parsed_url)
|
|
106
|
+
from redis.asyncio.cluster import RedisCluster
|
|
118
107
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
password = parsed.password
|
|
108
|
+
# 转换 URL scheme
|
|
109
|
+
redis_url = self._url.replace("redis-cluster://", "redis://")
|
|
122
110
|
|
|
123
|
-
# 处理 password@host
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
# 处理 password@host 格式(转换为标准 :password@host 格式)
|
|
112
|
+
parsed = urlparse(redis_url)
|
|
113
|
+
if parsed.username and not parsed.password:
|
|
114
|
+
# redis://password@host -> redis://:password@host
|
|
115
|
+
redis_url = redis_url.replace(
|
|
116
|
+
f"redis://{parsed.username}@",
|
|
117
|
+
f"redis://:{parsed.username}@"
|
|
118
|
+
)
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
self._redis = RedisCluster.from_url(
|
|
121
|
+
redis_url,
|
|
122
|
+
decode_responses=False,
|
|
123
|
+
socket_connect_timeout=30,
|
|
124
|
+
socket_timeout=30,
|
|
133
125
|
)
|
|
134
|
-
|
|
135
|
-
# 构建连接参数
|
|
136
|
-
cluster_kwargs: dict = {
|
|
137
|
-
"host": parsed.hostname or "localhost",
|
|
138
|
-
"port": parsed.port or 6379,
|
|
139
|
-
"decode_responses": False,
|
|
140
|
-
"connect_timeout": 5,
|
|
141
|
-
"stream_timeout": 5,
|
|
142
|
-
"retry_policy": retry_policy,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if username:
|
|
146
|
-
cluster_kwargs["username"] = username
|
|
147
|
-
if password:
|
|
148
|
-
cluster_kwargs["password"] = password
|
|
149
|
-
|
|
150
|
-
self._redis = RedisCluster(**cluster_kwargs)
|
|
151
126
|
self._is_cluster = True
|
|
152
127
|
|
|
153
128
|
async def get(self, key: str, default: Any = None) -> Any:
|
|
@@ -266,8 +241,11 @@ class RedisCache(ICache):
|
|
|
266
241
|
async def close(self) -> None:
|
|
267
242
|
"""关闭连接(仅当自己拥有连接时)。"""
|
|
268
243
|
if self._redis and self._owns_connection:
|
|
269
|
-
|
|
270
|
-
|
|
244
|
+
if self._is_cluster:
|
|
245
|
+
# redis-py cluster 使用 aclose()
|
|
246
|
+
await self._redis.aclose()
|
|
247
|
+
else:
|
|
248
|
+
await self._redis.close()
|
|
271
249
|
logger.info("Redis连接已关闭")
|
|
272
250
|
self._redis = None
|
|
273
251
|
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
支持的后端(通过 Broadcaster 库):
|
|
6
6
|
- memory:// - 内存通道(单进程,开发/测试用)
|
|
7
7
|
- redis:// - Redis Pub/Sub(多进程/分布式)
|
|
8
|
+
- redis-cluster:// - Redis Cluster(自动转换为普通 Pub/Sub)
|
|
8
9
|
- kafka:// - Apache Kafka
|
|
9
10
|
- postgres:// - PostgreSQL LISTEN/NOTIFY
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
|
-
from .backends import BroadcasterChannel
|
|
13
|
+
from .backends import BroadcasterChannel
|
|
13
14
|
from .base import ChannelBackend, ChannelMessage, IChannel
|
|
14
15
|
from .manager import ChannelManager
|
|
15
16
|
|
|
@@ -22,5 +23,4 @@ __all__ = [
|
|
|
22
23
|
"ChannelManager",
|
|
23
24
|
# 后端实现
|
|
24
25
|
"BroadcasterChannel",
|
|
25
|
-
"RedisClusterChannel",
|
|
26
26
|
]
|
|
@@ -16,10 +16,8 @@ from typing import Any
|
|
|
16
16
|
class ChannelBackend(Enum):
|
|
17
17
|
"""通道后端类型。"""
|
|
18
18
|
|
|
19
|
-
# Broadcaster 统一后端(支持 memory/redis/kafka/postgres,通过 URL scheme 区分)
|
|
19
|
+
# Broadcaster 统一后端(支持 memory/redis/redis-cluster/kafka/postgres,通过 URL scheme 区分)
|
|
20
20
|
BROADCASTER = "broadcaster"
|
|
21
|
-
# Redis Cluster + Sharded Pub/Sub (Redis 7.0+),使用 coredis 库
|
|
22
|
-
REDIS_CLUSTER = "redis_cluster"
|
|
23
21
|
# 未来扩展
|
|
24
22
|
RABBITMQ = "rabbitmq"
|
|
25
23
|
ROCKETMQ = "rocketmq"
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
|
+
from urllib.parse import urlparse
|
|
9
10
|
|
|
10
11
|
from aury.boot.common.logging import logger
|
|
11
12
|
|
|
12
13
|
from .backends.broadcaster import BroadcasterChannel
|
|
13
|
-
from .backends.redis_cluster_channel import RedisClusterChannel
|
|
14
14
|
from .base import ChannelBackend, ChannelMessage, IChannel
|
|
15
15
|
|
|
16
16
|
|
|
@@ -94,7 +94,7 @@ class ChannelManager:
|
|
|
94
94
|
url: 连接 URL,支持:
|
|
95
95
|
- memory:// - 内存后端(单进程,默认)
|
|
96
96
|
- redis://host:port/db - Redis Pub/Sub
|
|
97
|
-
- redis-cluster://[password@]host:port - Redis Cluster (
|
|
97
|
+
- redis-cluster://[password@]host:port - Redis Cluster (普通 Pub/Sub)
|
|
98
98
|
- kafka://host:port - Apache Kafka
|
|
99
99
|
- postgres://user:pass@host/db - PostgreSQL
|
|
100
100
|
|
|
@@ -109,17 +109,25 @@ class ChannelManager:
|
|
|
109
109
|
if isinstance(backend, str):
|
|
110
110
|
backend = ChannelBackend(backend.lower())
|
|
111
111
|
|
|
112
|
-
#
|
|
112
|
+
# redis-cluster:// 转换为 redis://,使用 broadcaster 的普通 Pub/Sub
|
|
113
|
+
# 普通 Pub/Sub 在 Redis Cluster 中会自动广播到所有节点
|
|
114
|
+
broadcast_url = url
|
|
113
115
|
if url.startswith("redis-cluster://"):
|
|
114
|
-
|
|
116
|
+
broadcast_url = url.replace("redis-cluster://", "redis://")
|
|
117
|
+
# 处理 password@host 格式(转换为标准 :password@host 格式)
|
|
118
|
+
parsed = urlparse(broadcast_url)
|
|
119
|
+
if parsed.username and not parsed.password:
|
|
120
|
+
broadcast_url = broadcast_url.replace(
|
|
121
|
+
f"redis://{parsed.username}@",
|
|
122
|
+
f"redis://:{parsed.username}@"
|
|
123
|
+
)
|
|
124
|
+
logger.info(f"通道管理器 [{self.name}] Redis Cluster 使用普通 Pub/Sub 模式")
|
|
115
125
|
|
|
116
126
|
self._backend_type = backend
|
|
117
127
|
self._url = url
|
|
118
128
|
|
|
119
|
-
if backend == ChannelBackend.BROADCASTER:
|
|
120
|
-
self._backend = BroadcasterChannel(
|
|
121
|
-
elif backend == ChannelBackend.REDIS_CLUSTER:
|
|
122
|
-
self._backend = RedisClusterChannel(url)
|
|
129
|
+
if backend == ChannelBackend.BROADCASTER or url.startswith("redis-cluster://"):
|
|
130
|
+
self._backend = BroadcasterChannel(broadcast_url)
|
|
123
131
|
elif backend in (ChannelBackend.RABBITMQ, ChannelBackend.ROCKETMQ):
|
|
124
132
|
raise NotImplementedError(f"{backend.value} 后端暂未实现")
|
|
125
133
|
else:
|
|
@@ -202,58 +202,32 @@ class RedisClient:
|
|
|
202
202
|
self._is_cluster = False
|
|
203
203
|
|
|
204
204
|
async def _initialize_cluster(self, url: str) -> None:
|
|
205
|
-
"""初始化 Redis Cluster 连接(使用
|
|
205
|
+
"""初始化 Redis Cluster 连接(使用 redis-py)。
|
|
206
206
|
|
|
207
207
|
支持 URL 格式:
|
|
208
208
|
- redis-cluster://password@host:port (密码在用户名位置)
|
|
209
209
|
- redis-cluster://:password@host:port (标准格式)
|
|
210
210
|
- redis-cluster://username:password@host:port (ACL 模式)
|
|
211
211
|
"""
|
|
212
|
-
|
|
213
|
-
from coredis import RedisCluster
|
|
214
|
-
from coredis.retry import ConstantRetryPolicy
|
|
215
|
-
from coredis.exceptions import ConnectionError as CoredisConnectionError
|
|
216
|
-
except ImportError as exc:
|
|
217
|
-
raise ImportError(
|
|
218
|
-
"Redis Cluster 需要安装 coredis: pip install coredis"
|
|
219
|
-
) from exc
|
|
220
|
-
|
|
221
|
-
# 解析 URL
|
|
222
|
-
parsed_url = url.replace("redis-cluster://", "redis://")
|
|
223
|
-
parsed = urlparse(parsed_url)
|
|
212
|
+
from redis.asyncio.cluster import RedisCluster
|
|
224
213
|
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
password = parsed.password
|
|
214
|
+
# 转换 URL scheme
|
|
215
|
+
redis_url = url.replace("redis-cluster://", "redis://")
|
|
228
216
|
|
|
229
|
-
# 处理 password@host
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
217
|
+
# 处理 password@host 格式(转换为标准 :password@host 格式)
|
|
218
|
+
parsed = urlparse(redis_url)
|
|
219
|
+
if parsed.username and not parsed.password:
|
|
220
|
+
redis_url = redis_url.replace(
|
|
221
|
+
f"redis://{parsed.username}@",
|
|
222
|
+
f"redis://:{parsed.username}@"
|
|
223
|
+
)
|
|
233
224
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
225
|
+
self._redis = RedisCluster.from_url(
|
|
226
|
+
redis_url,
|
|
227
|
+
decode_responses=self._config.decode_responses,
|
|
228
|
+
socket_connect_timeout=self._config.socket_connect_timeout or 30,
|
|
229
|
+
socket_timeout=self._config.socket_timeout or 30,
|
|
239
230
|
)
|
|
240
|
-
|
|
241
|
-
# 构建连接参数
|
|
242
|
-
cluster_kwargs: dict = {
|
|
243
|
-
"host": parsed.hostname or "localhost",
|
|
244
|
-
"port": parsed.port or 6379,
|
|
245
|
-
"decode_responses": self._config.decode_responses,
|
|
246
|
-
"connect_timeout": self._config.socket_connect_timeout or 5,
|
|
247
|
-
"stream_timeout": self._config.socket_timeout or 5,
|
|
248
|
-
"retry_policy": retry_policy,
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if username:
|
|
252
|
-
cluster_kwargs["username"] = username
|
|
253
|
-
if password:
|
|
254
|
-
cluster_kwargs["password"] = password
|
|
255
|
-
|
|
256
|
-
self._redis = RedisCluster(**cluster_kwargs)
|
|
257
231
|
self._is_cluster = True
|
|
258
232
|
|
|
259
233
|
def _mask_url(self, url: str) -> str:
|
|
@@ -328,8 +302,8 @@ class RedisClient:
|
|
|
328
302
|
"""清理资源,关闭连接。"""
|
|
329
303
|
if self._redis:
|
|
330
304
|
if self._is_cluster:
|
|
331
|
-
#
|
|
332
|
-
await self._redis.
|
|
305
|
+
# redis-py cluster 使用 aclose()
|
|
306
|
+
await self._redis.aclose()
|
|
333
307
|
else:
|
|
334
308
|
await self._redis.close()
|
|
335
309
|
logger.info(f"Redis 客户端 [{self.name}] 已关闭")
|
|
@@ -309,23 +309,12 @@ class RedisStreamMQ(IMQ):
|
|
|
309
309
|
await self._ensure_client()
|
|
310
310
|
stream_key = self._stream_key(queue)
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
end=end,
|
|
319
|
-
count=count,
|
|
320
|
-
)
|
|
321
|
-
else:
|
|
322
|
-
# redis-py API
|
|
323
|
-
result = await self._client.connection.xrange(
|
|
324
|
-
stream_key,
|
|
325
|
-
min=start,
|
|
326
|
-
max=end,
|
|
327
|
-
count=count,
|
|
328
|
-
)
|
|
312
|
+
result = await self._client.connection.xrange(
|
|
313
|
+
stream_key,
|
|
314
|
+
min=start,
|
|
315
|
+
max=end,
|
|
316
|
+
count=count,
|
|
317
|
+
)
|
|
329
318
|
|
|
330
319
|
messages = []
|
|
331
320
|
for msg_id, data in result:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aury-boot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.45
|
|
4
4
|
Summary: Aury Boot - 基于 FastAPI 生态的企业级 API 开发框架
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Requires-Dist: aiohttp>=3.11.0
|
|
@@ -28,7 +28,6 @@ Requires-Dist: aiosqlite>=0.21.0; extra == 'all'
|
|
|
28
28
|
Requires-Dist: apscheduler>=3.11.1; extra == 'all'
|
|
29
29
|
Requires-Dist: asyncpg>=0.31.0; extra == 'all'
|
|
30
30
|
Requires-Dist: aury-sdk-storage[aws]>=0.0.1; extra == 'all'
|
|
31
|
-
Requires-Dist: coredis>=5.6.0; extra == 'all'
|
|
32
31
|
Requires-Dist: dramatiq>=1.18.0; extra == 'all'
|
|
33
32
|
Requires-Dist: pika>=1.3.2; extra == 'all'
|
|
34
33
|
Requires-Dist: psutil>=7.0.0; extra == 'all'
|
|
@@ -36,8 +35,6 @@ Requires-Dist: pyroscope-io>=0.8.7; extra == 'all'
|
|
|
36
35
|
Requires-Dist: redis>=7.1.0; extra == 'all'
|
|
37
36
|
Provides-Extra: broadcaster
|
|
38
37
|
Requires-Dist: broadcaster[redis]>=0.3.1; extra == 'broadcaster'
|
|
39
|
-
Provides-Extra: channel-cluster
|
|
40
|
-
Requires-Dist: coredis>=5.6.0; extra == 'channel-cluster'
|
|
41
38
|
Provides-Extra: dev
|
|
42
39
|
Requires-Dist: httpx>=0.28.1; extra == 'dev'
|
|
43
40
|
Requires-Dist: mypy>=1.19.0; extra == 'dev'
|
|
@@ -71,7 +68,6 @@ Requires-Dist: aiosqlite>=0.21.0; extra == 'recommended'
|
|
|
71
68
|
Requires-Dist: apscheduler>=3.11.1; extra == 'recommended'
|
|
72
69
|
Requires-Dist: asyncpg>=0.31.0; extra == 'recommended'
|
|
73
70
|
Requires-Dist: aury-sdk-storage[aws]>=0.0.1; extra == 'recommended'
|
|
74
|
-
Requires-Dist: coredis>=5.6.0; extra == 'recommended'
|
|
75
71
|
Requires-Dist: dramatiq>=1.18.0; extra == 'recommended'
|
|
76
72
|
Requires-Dist: opentelemetry-api>=1.25.0; extra == 'recommended'
|
|
77
73
|
Requires-Dist: opentelemetry-instrumentation-aiohttp-client>=0.46b0; extra == 'recommended'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
|
|
2
|
-
aury/boot/_version.py,sha256=
|
|
2
|
+
aury/boot/_version.py,sha256=ZyfWnYPDH177nyt27Ffy71kE1qNlQLtAEIFxyT_Cx5c,706
|
|
3
3
|
aury/boot/application/__init__.py,sha256=I2KqNVdYg2q5nlOXr0TtFGyHmhj4oWdaR6ZB73Mwg7Y,3041
|
|
4
4
|
aury/boot/application/adapter/__init__.py,sha256=e1bcSb1bxUMfofTwiCuHBZJk5-STkMCWPF2EJXHQ7UU,3976
|
|
5
5
|
aury/boot/application/adapter/base.py,sha256=Ar_66fiHPDEmV-1DKnqXKwc53p3pozG31bgTJTEUriY,15763
|
|
@@ -143,21 +143,19 @@ aury/boot/infrastructure/cache/exceptions.py,sha256=KZsFIHXW3_kOh_KB93EVZJKbiDvD
|
|
|
143
143
|
aury/boot/infrastructure/cache/factory.py,sha256=aF74JoiiSKFgctqqh2Z8OtGRS2Am_ou-I40GyygLzC0,2489
|
|
144
144
|
aury/boot/infrastructure/cache/manager.py,sha256=2jlshbO4NqpPxH-8DBiMFNAvWuZUI3atUCsw9GGlzc8,16807
|
|
145
145
|
aury/boot/infrastructure/cache/memory.py,sha256=qGhLKKjGsEUHjVRFMV6A33MB_1iPaKCEEkT6VFrLkQY,9832
|
|
146
|
-
aury/boot/infrastructure/cache/redis.py,sha256=
|
|
147
|
-
aury/boot/infrastructure/channel/__init__.py,sha256=
|
|
148
|
-
aury/boot/infrastructure/channel/base.py,sha256=
|
|
149
|
-
aury/boot/infrastructure/channel/manager.py,sha256=
|
|
150
|
-
aury/boot/infrastructure/channel/backends/__init__.py,sha256=
|
|
146
|
+
aury/boot/infrastructure/cache/redis.py,sha256=mV2al54ZkiapIF3oBaFLTQAmZbpkxV5CCpS6YJrg75Y,10283
|
|
147
|
+
aury/boot/infrastructure/channel/__init__.py,sha256=gl0PNOEfomiuVh5KLwJKoc3r_vnQsoXKHuwJBffgfMY,734
|
|
148
|
+
aury/boot/infrastructure/channel/base.py,sha256=Us1sqa6__XUXPNaI_Fd8beznEyGu8ezkLZBjqIix5HQ,2955
|
|
149
|
+
aury/boot/infrastructure/channel/manager.py,sha256=S70W9q86RCWamhwRQxqyGMC-Nvlv7aKTXw5R_ev4qZg,8441
|
|
150
|
+
aury/boot/infrastructure/channel/backends/__init__.py,sha256=NcXG8_KAqy1SiGUs2z_KvkS90jMfLJ6bzyYK4Jw4qCg,107
|
|
151
151
|
aury/boot/infrastructure/channel/backends/broadcaster.py,sha256=y8eKx6X6Iy9a_5vnLMm5gjqkq05SmJEWESw1-x0lIFg,4771
|
|
152
|
-
aury/boot/infrastructure/channel/backends/redis_cluster.py,sha256=KtPB0nHK76tyVFiD2Rs_LPbjyw0R-9CwM0R-xcy9cTY,4579
|
|
153
|
-
aury/boot/infrastructure/channel/backends/redis_cluster_channel.py,sha256=MAlo1mnyQzjhK1FwxeTiMvz_RpFjVuH3AgKKhAcH8Rs,4949
|
|
154
152
|
aury/boot/infrastructure/clients/__init__.py,sha256=1ANMejb3RrBgaR-jq-dsxJ0kQDRHz5jV-QvdUNcf_ok,435
|
|
155
153
|
aury/boot/infrastructure/clients/rabbitmq/__init__.py,sha256=cnU-W7jOcAgp_FvsY9EipNCeJzeA9gHLRuZ0yQZE2DI,200
|
|
156
154
|
aury/boot/infrastructure/clients/rabbitmq/config.py,sha256=YmvNiISpqNt-LE2CrpzmxCgaEgYna7IbOfUSnA0B4T0,1239
|
|
157
155
|
aury/boot/infrastructure/clients/rabbitmq/manager.py,sha256=a3Op0yN2DICnoqxOVb0DVT9RnoF8laN2EutOsOSWzWA,9659
|
|
158
156
|
aury/boot/infrastructure/clients/redis/__init__.py,sha256=HGZVfcWmOPeiAk-rJ8Yun7N5CQiPlGFofdByvl8Uqso,613
|
|
159
157
|
aury/boot/infrastructure/clients/redis/config.py,sha256=KfC2R7bcQ91zjTp8Q_S7j3ZemDLdejUYc3CrWsJlpNM,1421
|
|
160
|
-
aury/boot/infrastructure/clients/redis/manager.py,sha256=
|
|
158
|
+
aury/boot/infrastructure/clients/redis/manager.py,sha256=4c0Xknkcrg3Ly4bN3vIP8N8IVS6vFTwUwTSH58e2bUg,10824
|
|
161
159
|
aury/boot/infrastructure/database/__init__.py,sha256=MsHNyrJ2CZJT-lbVZzOAJ0nFfFEmHrJqC0zw-cFS768,888
|
|
162
160
|
aury/boot/infrastructure/database/config.py,sha256=5LYy4DuLL0XNjVnX2HUcrMh3c71eeZa-vWGM8QCkL0U,1408
|
|
163
161
|
aury/boot/infrastructure/database/exceptions.py,sha256=hUjsU23c0eMwogSDrKq_bQ6zvnY7PQSGaitbCEhhDZQ,766
|
|
@@ -197,7 +195,7 @@ aury/boot/infrastructure/mq/manager.py,sha256=Bu4E1Tgz0CzFvJuCS9_fBMj9eAqmXcZp8a
|
|
|
197
195
|
aury/boot/infrastructure/mq/backends/__init__.py,sha256=10nggw2V-AzuZ1vvzq_ksoXR4FI3e4BR36EfY49Pek4,200
|
|
198
196
|
aury/boot/infrastructure/mq/backends/rabbitmq.py,sha256=0NWgPKEwtbmI63EVvKINdfXXDNyOvuOOP9LlBzqH91E,5493
|
|
199
197
|
aury/boot/infrastructure/mq/backends/redis.py,sha256=B89U7mqIceUsCXE4G3u1u6aFM9hv4mmLLwuCYq1T9tQ,5281
|
|
200
|
-
aury/boot/infrastructure/mq/backends/redis_stream.py,sha256=
|
|
198
|
+
aury/boot/infrastructure/mq/backends/redis_stream.py,sha256=p2WTj10-zbxQ_2NPU97w-n4DZ8KSHhLjqcnplLPCw4U,14761
|
|
201
199
|
aury/boot/infrastructure/scheduler/__init__.py,sha256=ji_K1OePMHt4CIFr168LGEbSuX8ybgrden-W75b0NdI,395
|
|
202
200
|
aury/boot/infrastructure/scheduler/exceptions.py,sha256=ROltrhSctVWA-6ulnjuYeHAk3ZF-sykDoesuierYzew,634
|
|
203
201
|
aury/boot/infrastructure/scheduler/manager.py,sha256=wUxMRGXpoAwjHnB4u7BKnzJbiPZE5sovuLPrgLoQYb4,23753
|
|
@@ -218,7 +216,7 @@ aury/boot/testing/client.py,sha256=KOg1EemuIVsBG68G5y0DjSxZGcIQVdWQ4ASaHE3o1R0,4
|
|
|
218
216
|
aury/boot/testing/factory.py,sha256=8GvwX9qIDu0L65gzJMlrWB0xbmJ-7zPHuwk3eECULcg,5185
|
|
219
217
|
aury/boot/toolkit/__init__.py,sha256=AcyVb9fDf3CaEmJPNkWC4iGv32qCPyk4BuFKSuNiJRQ,334
|
|
220
218
|
aury/boot/toolkit/http/__init__.py,sha256=5bv4Ntz1sbNFhP9zPLBDhB536ZX1CKIAOp-kQSKMRQ0,14161
|
|
221
|
-
aury_boot-0.0.
|
|
222
|
-
aury_boot-0.0.
|
|
223
|
-
aury_boot-0.0.
|
|
224
|
-
aury_boot-0.0.
|
|
219
|
+
aury_boot-0.0.45.dist-info/METADATA,sha256=6s2k9vknMZuXxUUOw3m2jXPk_v5xwQ8IdymHWWIxkss,8989
|
|
220
|
+
aury_boot-0.0.45.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
221
|
+
aury_boot-0.0.45.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
|
|
222
|
+
aury_boot-0.0.45.dist-info/RECORD,,
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"""Redis Cluster 通道后端。
|
|
2
|
-
|
|
3
|
-
使用 coredis 库支持异步 Redis Cluster Sharded Pub/Sub (Redis 7.0+)。
|
|
4
|
-
|
|
5
|
-
URL 格式:
|
|
6
|
-
redis-cluster://password@host:port
|
|
7
|
-
redis-cluster://host:port
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from typing import TYPE_CHECKING, Any
|
|
15
|
-
from urllib.parse import urlparse
|
|
16
|
-
|
|
17
|
-
from aury.boot.common.logging import logger
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
from coredis import RedisCluster
|
|
21
|
-
from coredis.retry import ConstantRetryPolicy
|
|
22
|
-
from coredis.exceptions import ConnectionError as CoredisConnectionError
|
|
23
|
-
except ImportError as exc:
|
|
24
|
-
raise ImportError(
|
|
25
|
-
"Redis Cluster Channel 需要安装 coredis: pip install coredis"
|
|
26
|
-
) from exc
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass
|
|
30
|
-
class Event:
|
|
31
|
-
"""与 broadcaster._base.Event 兼容的事件类。"""
|
|
32
|
-
channel: str
|
|
33
|
-
message: str
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class RedisClusterBackend:
|
|
37
|
-
"""Redis Cluster Pub/Sub 后端。
|
|
38
|
-
|
|
39
|
-
使用 coredis 库的 sharded_pubsub() 支持 Sharded Pub/Sub。
|
|
40
|
-
与 broadcaster.RedisBackend 接口兼容。
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(self, url: str) -> None:
|
|
44
|
-
host, port, password = self._parse_url(url)
|
|
45
|
-
|
|
46
|
-
# 配置更快的重试策略
|
|
47
|
-
retry_policy = ConstantRetryPolicy(
|
|
48
|
-
retries=3,
|
|
49
|
-
delay=1,
|
|
50
|
-
retryable_exceptions=(CoredisConnectionError, TimeoutError, OSError),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
self._client: RedisCluster = RedisCluster(
|
|
54
|
-
host=host,
|
|
55
|
-
port=port,
|
|
56
|
-
password=password,
|
|
57
|
-
decode_responses=True,
|
|
58
|
-
connect_timeout=5,
|
|
59
|
-
stream_timeout=5,
|
|
60
|
-
retry_policy=retry_policy,
|
|
61
|
-
)
|
|
62
|
-
self._pubsub: Any = None
|
|
63
|
-
self._queue: asyncio.Queue[Event] = asyncio.Queue()
|
|
64
|
-
self._listener_task: asyncio.Task | None = None
|
|
65
|
-
self._subscribed_channels: set[str] = set()
|
|
66
|
-
|
|
67
|
-
def _parse_url(self, url: str) -> tuple[str, int, str | None]:
|
|
68
|
-
"""解析 URL。"""
|
|
69
|
-
url = url.replace("redis-cluster://", "redis://")
|
|
70
|
-
parsed = urlparse(url)
|
|
71
|
-
password = parsed.password
|
|
72
|
-
# 支持 password@host 格式
|
|
73
|
-
if not password and parsed.username:
|
|
74
|
-
password = parsed.username
|
|
75
|
-
return parsed.hostname or "localhost", parsed.port or 6379, password
|
|
76
|
-
|
|
77
|
-
async def connect(self) -> None:
|
|
78
|
-
"""连接并初始化 pubsub。"""
|
|
79
|
-
# coredis 的 sharded_pubsub() 支持 Redis 7.0+ Sharded Pub/Sub
|
|
80
|
-
self._pubsub = self._client.sharded_pubsub()
|
|
81
|
-
self._listener_task = asyncio.create_task(self._listen_loop())
|
|
82
|
-
logger.debug("Redis Cluster Channel 已连接")
|
|
83
|
-
|
|
84
|
-
async def disconnect(self) -> None:
|
|
85
|
-
"""断开连接。"""
|
|
86
|
-
if self._listener_task:
|
|
87
|
-
self._listener_task.cancel()
|
|
88
|
-
try:
|
|
89
|
-
await self._listener_task
|
|
90
|
-
except asyncio.CancelledError:
|
|
91
|
-
pass
|
|
92
|
-
if self._pubsub:
|
|
93
|
-
await self._pubsub.sunsubscribe()
|
|
94
|
-
await self._client.close()
|
|
95
|
-
logger.debug("Redis Cluster Channel 已断开")
|
|
96
|
-
|
|
97
|
-
async def subscribe(self, channel: str) -> None:
|
|
98
|
-
"""订阅频道。"""
|
|
99
|
-
if self._pubsub:
|
|
100
|
-
await self._pubsub.ssubscribe(channel)
|
|
101
|
-
self._subscribed_channels.add(channel)
|
|
102
|
-
|
|
103
|
-
async def unsubscribe(self, channel: str) -> None:
|
|
104
|
-
"""取消订阅。"""
|
|
105
|
-
if self._pubsub and channel in self._subscribed_channels:
|
|
106
|
-
await self._pubsub.sunsubscribe(channel)
|
|
107
|
-
self._subscribed_channels.discard(channel)
|
|
108
|
-
|
|
109
|
-
async def publish(self, channel: str, message: str) -> None:
|
|
110
|
-
"""发布消息(使用 SPUBLISH)。"""
|
|
111
|
-
await self._client.spublish(channel, message)
|
|
112
|
-
|
|
113
|
-
async def next_published(self) -> Event:
|
|
114
|
-
"""获取下一条消息。"""
|
|
115
|
-
return await self._queue.get()
|
|
116
|
-
|
|
117
|
-
async def _listen_loop(self) -> None:
|
|
118
|
-
"""监听消息循环。"""
|
|
119
|
-
while True:
|
|
120
|
-
try:
|
|
121
|
-
if self._pubsub:
|
|
122
|
-
async for message in self._pubsub:
|
|
123
|
-
if message and message.get("type") in ("message", "smessage"):
|
|
124
|
-
event = Event(
|
|
125
|
-
channel=message.get("channel", ""),
|
|
126
|
-
message=message.get("data", ""),
|
|
127
|
-
)
|
|
128
|
-
await self._queue.put(event)
|
|
129
|
-
except asyncio.CancelledError:
|
|
130
|
-
break
|
|
131
|
-
except Exception as e:
|
|
132
|
-
logger.warning(f"Redis Cluster pubsub error: {e}")
|
|
133
|
-
await asyncio.sleep(1)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
__all__ = ["RedisClusterBackend"]
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
"""Redis Cluster 通道封装。
|
|
2
|
-
|
|
3
|
-
使用 RedisClusterBackend,提供与 BroadcasterChannel 一致的接口。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
from collections.abc import AsyncIterator
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
|
|
12
|
-
from aury.boot.common.logging import logger
|
|
13
|
-
|
|
14
|
-
from ..base import ChannelMessage, IChannel
|
|
15
|
-
from .redis_cluster import RedisClusterBackend
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class RedisClusterChannel(IChannel):
|
|
19
|
-
"""Redis Cluster 通道实现。
|
|
20
|
-
|
|
21
|
-
使用 broadcaster 架构:共享连接 + Queue 分发。
|
|
22
|
-
支持 Sharded Pub/Sub (Redis 7.0+)。
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
def __init__(self, url: str) -> None:
|
|
26
|
-
"""初始化 Redis Cluster 通道。
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
url: redis-cluster://[password@]host:port
|
|
30
|
-
"""
|
|
31
|
-
self._url = url
|
|
32
|
-
self._backend = RedisClusterBackend(url)
|
|
33
|
-
self._connected = False
|
|
34
|
-
# 订阅者管理(与 broadcaster 相同的模式)
|
|
35
|
-
self._subscribers: dict[str, set] = {}
|
|
36
|
-
self._listener_task = None
|
|
37
|
-
|
|
38
|
-
async def _ensure_connected(self) -> None:
|
|
39
|
-
if not self._connected:
|
|
40
|
-
await self._backend.connect()
|
|
41
|
-
self._listener_task = __import__("asyncio").create_task(self._listener())
|
|
42
|
-
self._connected = True
|
|
43
|
-
logger.debug(f"Redis Cluster 通道已连接: {self._mask_url(self._url)}")
|
|
44
|
-
|
|
45
|
-
def _mask_url(self, url: str) -> str:
|
|
46
|
-
if "@" in url:
|
|
47
|
-
parts = url.split("@")
|
|
48
|
-
prefix = parts[0]
|
|
49
|
-
suffix = parts[1]
|
|
50
|
-
if "://" in prefix:
|
|
51
|
-
scheme = prefix.split("://")[0]
|
|
52
|
-
return f"{scheme}://***@{suffix}"
|
|
53
|
-
return url
|
|
54
|
-
|
|
55
|
-
async def _listener(self) -> None:
|
|
56
|
-
"""监听后端消息,分发到订阅者。"""
|
|
57
|
-
import asyncio
|
|
58
|
-
while True:
|
|
59
|
-
try:
|
|
60
|
-
event = await self._backend.next_published()
|
|
61
|
-
channel = event.channel
|
|
62
|
-
if channel in self._subscribers:
|
|
63
|
-
for queue in list(self._subscribers[channel]):
|
|
64
|
-
await queue.put(event)
|
|
65
|
-
except asyncio.CancelledError:
|
|
66
|
-
break
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.warning(f"Redis Cluster listener error: {e}")
|
|
69
|
-
|
|
70
|
-
async def publish(self, channel: str, message: ChannelMessage) -> None:
|
|
71
|
-
await self._ensure_connected()
|
|
72
|
-
message.channel = channel
|
|
73
|
-
data = {
|
|
74
|
-
"data": message.data,
|
|
75
|
-
"event": message.event,
|
|
76
|
-
"id": message.id,
|
|
77
|
-
"channel": message.channel,
|
|
78
|
-
"timestamp": message.timestamp.isoformat(),
|
|
79
|
-
}
|
|
80
|
-
await self._backend.publish(channel, json.dumps(data))
|
|
81
|
-
|
|
82
|
-
async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
|
|
83
|
-
import asyncio
|
|
84
|
-
await self._ensure_connected()
|
|
85
|
-
|
|
86
|
-
queue: asyncio.Queue = asyncio.Queue()
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
# 首个订阅者时订阅 Redis
|
|
90
|
-
if channel not in self._subscribers:
|
|
91
|
-
await self._backend.subscribe(channel)
|
|
92
|
-
self._subscribers[channel] = set()
|
|
93
|
-
self._subscribers[channel].add(queue)
|
|
94
|
-
|
|
95
|
-
while True:
|
|
96
|
-
event = await queue.get()
|
|
97
|
-
try:
|
|
98
|
-
data = json.loads(event.message)
|
|
99
|
-
yield ChannelMessage(
|
|
100
|
-
data=data.get("data"),
|
|
101
|
-
event=data.get("event"),
|
|
102
|
-
id=data.get("id"),
|
|
103
|
-
channel=data.get("channel") or channel,
|
|
104
|
-
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
105
|
-
if data.get("timestamp")
|
|
106
|
-
else datetime.now(),
|
|
107
|
-
)
|
|
108
|
-
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
109
|
-
logger.warning(f"解析通道消息失败: {e}")
|
|
110
|
-
finally:
|
|
111
|
-
if channel in self._subscribers:
|
|
112
|
-
self._subscribers[channel].discard(queue)
|
|
113
|
-
if not self._subscribers[channel]:
|
|
114
|
-
del self._subscribers[channel]
|
|
115
|
-
try:
|
|
116
|
-
await self._backend.unsubscribe(channel)
|
|
117
|
-
except Exception:
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
|
-
async def psubscribe(self, pattern: str) -> AsyncIterator[ChannelMessage]:
|
|
121
|
-
raise NotImplementedError(
|
|
122
|
-
"Redis Cluster Sharded Pub/Sub 不支持模式订阅。"
|
|
123
|
-
"请使用具体的 channel 名称。"
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
async def unsubscribe(self, channel: str) -> None:
|
|
127
|
-
pass # subscribe() 的 finally 块自动处理
|
|
128
|
-
|
|
129
|
-
async def close(self) -> None:
|
|
130
|
-
if self._connected:
|
|
131
|
-
if self._listener_task:
|
|
132
|
-
self._listener_task.cancel()
|
|
133
|
-
await self._backend.disconnect()
|
|
134
|
-
self._connected = False
|
|
135
|
-
self._subscribers.clear()
|
|
136
|
-
logger.debug("Redis Cluster 通道已关闭")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
__all__ = ["RedisClusterChannel"]
|
|
File without changes
|
|
File without changes
|