aury-boot 0.0.43__py3-none-any.whl → 0.0.44__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 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.43'
32
- __version_tuple__ = version_tuple = (0, 0, 43)
31
+ __version__ = version = '0.0.44'
32
+ __version_tuple__ = version_tuple = (0, 0, 44)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -96,58 +96,31 @@ class RedisCache(ICache):
96
96
  self._is_cluster = False
97
97
 
98
98
  async def _init_cluster(self) -> None:
99
- """初始化 Redis Cluster(使用 coredis)。
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
- try:
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
- username = parsed.username
121
- password = parsed.password
108
+ # 转换 URL scheme
109
+ redis_url = self._url.replace("redis-cluster://", "redis://")
122
110
 
123
- # 处理 password@host 格式
124
- if username and not password:
125
- password = username
126
- username = None
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
- retry_policy = ConstantRetryPolicy(
130
- retries=3,
131
- delay=1,
132
- retryable_exceptions=(CoredisConnectionError, TimeoutError, OSError),
120
+ self._redis = RedisCluster.from_url(
121
+ redis_url,
122
+ decode_responses=False,
133
123
  )
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
124
  self._is_cluster = True
152
125
 
153
126
  async def get(self, key: str, default: Any = None) -> Any:
@@ -266,8 +239,11 @@ class RedisCache(ICache):
266
239
  async def close(self) -> None:
267
240
  """关闭连接(仅当自己拥有连接时)。"""
268
241
  if self._redis and self._owns_connection:
269
- # coredis 和 redis-py 都使用 close() 方法
270
- await self._redis.close()
242
+ if self._is_cluster:
243
+ # redis-py cluster 使用 aclose()
244
+ await self._redis.aclose()
245
+ else:
246
+ await self._redis.close()
271
247
  logger.info("Redis连接已关闭")
272
248
  self._redis = None
273
249
 
@@ -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, RedisClusterChannel
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
  ]
@@ -1,6 +1,5 @@
1
1
  """通道后端实现。"""
2
2
 
3
3
  from .broadcaster import BroadcasterChannel
4
- from .redis_cluster_channel import RedisClusterChannel
5
4
 
6
- __all__ = ["BroadcasterChannel", "RedisClusterChannel"]
5
+ __all__ = ["BroadcasterChannel"]
@@ -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"
@@ -10,7 +10,6 @@ from collections.abc import AsyncIterator
10
10
  from aury.boot.common.logging import logger
11
11
 
12
12
  from .backends.broadcaster import BroadcasterChannel
13
- from .backends.redis_cluster_channel import RedisClusterChannel
14
13
  from .base import ChannelBackend, ChannelMessage, IChannel
15
14
 
16
15
 
@@ -94,7 +93,7 @@ class ChannelManager:
94
93
  url: 连接 URL,支持:
95
94
  - memory:// - 内存后端(单进程,默认)
96
95
  - redis://host:port/db - Redis Pub/Sub
97
- - redis-cluster://[password@]host:port - Redis Cluster (Sharded Pub/Sub)
96
+ - redis-cluster://[password@]host:port - Redis Cluster (普通 Pub/Sub)
98
97
  - kafka://host:port - Apache Kafka
99
98
  - postgres://user:pass@host/db - PostgreSQL
100
99
 
@@ -109,17 +108,18 @@ class ChannelManager:
109
108
  if isinstance(backend, str):
110
109
  backend = ChannelBackend(backend.lower())
111
110
 
112
- # 自动检测 redis-cluster:// scheme
111
+ # redis-cluster:// 转换为 redis://,使用 broadcaster 的普通 Pub/Sub
112
+ # 普通 Pub/Sub 在 Redis Cluster 中会自动广播到所有节点
113
+ broadcast_url = url
113
114
  if url.startswith("redis-cluster://"):
114
- backend = ChannelBackend.REDIS_CLUSTER
115
+ broadcast_url = url.replace("redis-cluster://", "redis://")
116
+ logger.info(f"通道管理器 [{self.name}] Redis Cluster 使用普通 Pub/Sub 模式")
115
117
 
116
118
  self._backend_type = backend
117
119
  self._url = url
118
120
 
119
- if backend == ChannelBackend.BROADCASTER:
120
- self._backend = BroadcasterChannel(url)
121
- elif backend == ChannelBackend.REDIS_CLUSTER:
122
- self._backend = RedisClusterChannel(url)
121
+ if backend == ChannelBackend.BROADCASTER or url.startswith("redis-cluster://"):
122
+ self._backend = BroadcasterChannel(broadcast_url)
123
123
  elif backend in (ChannelBackend.RABBITMQ, ChannelBackend.ROCKETMQ):
124
124
  raise NotImplementedError(f"{backend.value} 后端暂未实现")
125
125
  else:
@@ -202,58 +202,30 @@ class RedisClient:
202
202
  self._is_cluster = False
203
203
 
204
204
  async def _initialize_cluster(self, url: str) -> None:
205
- """初始化 Redis Cluster 连接(使用 coredis)。
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
- try:
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
- username = parsed.username
227
- password = parsed.password
214
+ # 转换 URL scheme
215
+ redis_url = url.replace("redis-cluster://", "redis://")
228
216
 
229
- # 处理 password@host 格式
230
- if username and not password:
231
- password = username
232
- username = None
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
- retry_policy = ConstantRetryPolicy(
236
- retries=3,
237
- delay=1, # 1 秒重试间隔
238
- retryable_exceptions=(CoredisConnectionError, TimeoutError, OSError),
225
+ self._redis = RedisCluster.from_url(
226
+ redis_url,
227
+ decode_responses=self._config.decode_responses,
239
228
  )
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
229
  self._is_cluster = True
258
230
 
259
231
  def _mask_url(self, url: str) -> str:
@@ -328,8 +300,8 @@ class RedisClient:
328
300
  """清理资源,关闭连接。"""
329
301
  if self._redis:
330
302
  if self._is_cluster:
331
- # coredis 使用 close() 方法
332
- await self._redis.close()
303
+ # redis-py cluster 使用 aclose()
304
+ await self._redis.aclose()
333
305
  else:
334
306
  await self._redis.close()
335
307
  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
- # coredis 使用 start/end,redis-py 使用 min/max
313
- if hasattr(self._client, 'is_cluster') and self._client.is_cluster:
314
- # coredis API
315
- result = await self._client.connection.xrange(
316
- stream_key,
317
- start=start,
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.43
3
+ Version: 0.0.44
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=qJphZkKjg5qeGMzBrtqKwyJZQtcx3oaEh-0t6ejMuVo,706
2
+ aury/boot/_version.py,sha256=Zrt00MLeXbWP2ZsKSWktjZjWuHHC_-KLVmAWLgE0o-g,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=OeLrjQ8jooJLItTr5xtgoxpb6WPw-HN3JpGNPQDvINA,10964
147
- aury/boot/infrastructure/channel/__init__.py,sha256=zv3dNH4By8h-5Fksi18LFJVf5cXBmj0M49DjTfQL2w4,712
148
- aury/boot/infrastructure/channel/base.py,sha256=xE_7vtgSB06PIq6HPh59MXXisjBuz530NmlnMwQxU5Q,3049
149
- aury/boot/infrastructure/channel/manager.py,sha256=pTXhWTjRh83ifX9bKlsD4t5w1pZQOZszGbiy4wBYfJ8,7906
150
- aury/boot/infrastructure/channel/backends/__init__.py,sha256=qRA8JuAueQXRfcqJSl-M2a1sRNscPjCQBgVmWn3DFu4,185
146
+ aury/boot/infrastructure/cache/redis.py,sha256=OtjDRVSGI3WFrURTEVa-2NabZMPywjKTeZuNDjloN1I,10213
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=BmWmd4h7451JbdoJy4p0SRrSTpzIbNpqVAGATxxlzfI,8046
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=b8x-SD8wS_YSCjneS_63rCzRZOmojqxKiV0zpdjTJ4U,11697
158
+ aury/boot/infrastructure/clients/redis/manager.py,sha256=MT-a_3wIw7DSPWyAs_G8WT1UpoVGN-_G8iyN7It8rbE,10684
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=IAw-sn87uIV9YI65WaHF5dCTNdQd3hKWFcZI4AUCNvk,15173
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.43.dist-info/METADATA,sha256=WJz98j7foEbb61Ih5wMbApuApunDPlfEPiMjwNyBAvU,9179
222
- aury_boot-0.0.43.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
223
- aury_boot-0.0.43.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
224
- aury_boot-0.0.43.dist-info/RECORD,,
219
+ aury_boot-0.0.44.dist-info/METADATA,sha256=bfqYiQIMM0PCiv8FBeYWG_E-7MnPapRsErR0wu0TNP0,8989
220
+ aury_boot-0.0.44.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
221
+ aury_boot-0.0.44.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
222
+ aury_boot-0.0.44.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"]