aury-boot 0.0.36__py3-none-any.whl → 0.0.38__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.
@@ -1,428 +1,10 @@
1
- """缓存后端实现。
1
+ """缓存后端实现(兼容层)。
2
2
 
3
- 提供 Redis、Memory、Memcached 等缓存后端的实现。
3
+ 实际实现在 redis.py 和 memory.py 中。
4
4
  """
5
5
 
6
- from __future__ import annotations
7
-
8
- import asyncio
9
- from collections.abc import Callable
10
- from datetime import timedelta
11
- import json
12
- import pickle
13
- from typing import TYPE_CHECKING, Any
14
-
15
- from redis.asyncio import Redis
16
-
17
- from aury.boot.common.logging import logger
18
-
19
- from .base import ICache
20
-
21
- if TYPE_CHECKING:
22
- from aury.boot.infrastructure.clients.redis import RedisClient
23
-
24
-
25
- class RedisCache(ICache):
26
- """Redis缓存实现。
27
-
28
- 支持两种初始化方式:
29
- 1. 传入 URL 自行创建连接
30
- 2. 传入 RedisClient 实例(推荐)
31
- """
32
-
33
- def __init__(
34
- self,
35
- url: str | None = None,
36
- *,
37
- redis_client: RedisClient | None = None,
38
- serializer: str = "json",
39
- ):
40
- """初始化Redis缓存。
41
-
42
- Args:
43
- url: Redis连接URL
44
- redis_client: RedisClient 实例(推荐)
45
- serializer: 序列化方式(json/pickle)
46
- """
47
- self._url = url
48
- self._redis_client = redis_client
49
- self._serializer = serializer
50
- self._redis: Redis | None = None
51
- self._owns_connection = False # 是否自己拥有连接(需要自己关闭)
52
-
53
- async def initialize(self) -> None:
54
- """初始化连接。"""
55
- # 优先使用 RedisClient
56
- if self._redis_client is not None:
57
- self._redis = self._redis_client.connection
58
- self._owns_connection = False
59
- logger.info("Redis缓存初始化成功(使用 RedisClient)")
60
- return
61
-
62
- # 使用 URL 创建连接
63
- if self._url:
64
- try:
65
- self._redis = Redis.from_url(
66
- self._url,
67
- encoding="utf-8",
68
- decode_responses=False,
69
- socket_connect_timeout=5,
70
- socket_timeout=5,
71
- )
72
- await self._redis.ping()
73
- self._owns_connection = True
74
- logger.info("Redis缓存初始化成功")
75
- except Exception as exc:
76
- logger.error(f"Redis连接失败: {exc}")
77
- raise
78
- else:
79
- raise ValueError("Redis缓存需要提供 url 或 redis_client 参数")
80
-
81
- async def get(self, key: str, default: Any = None) -> Any:
82
- """获取缓存。"""
83
- if not self._redis:
84
- return default
85
-
86
- try:
87
- data = await self._redis.get(key)
88
- if data is None:
89
- return default
90
-
91
- # 使用函数式编程处理序列化器
92
- deserializers: dict[str, Callable[[bytes], Any]] = {
93
- "json": lambda d: json.loads(d.decode()),
94
- "pickle": pickle.loads,
95
- }
96
-
97
- deserializer = deserializers.get(self._serializer)
98
- if deserializer:
99
- return deserializer(data)
100
- return data.decode()
101
- except Exception as exc:
102
- logger.error(f"Redis获取失败: {key}, {exc}")
103
- return default
104
-
105
- async def set(
106
- self,
107
- key: str,
108
- value: Any,
109
- expire: int | timedelta | None = None,
110
- ) -> bool:
111
- """设置缓存。"""
112
- if not self._redis:
113
- return False
114
-
115
- try:
116
- # 使用函数式编程处理序列化器
117
- serializers: dict[str, Callable[[Any], bytes]] = {
118
- "json": lambda v: json.dumps(v).encode(),
119
- "pickle": pickle.dumps,
120
- }
121
-
122
- serializer = serializers.get(self._serializer)
123
- if serializer:
124
- data = serializer(value)
125
- else:
126
- data = str(value).encode()
127
-
128
- # 转换过期时间
129
- if isinstance(expire, timedelta):
130
- expire = int(expire.total_seconds())
131
-
132
- await self._redis.set(key, data, ex=expire)
133
- return True
134
- except Exception as exc:
135
- logger.error(f"Redis设置失败: {key}, {exc}")
136
- return False
137
-
138
- async def delete(self, *keys: str) -> int:
139
- """删除缓存。"""
140
- if not self._redis or not keys:
141
- return 0
142
-
143
- try:
144
- return await self._redis.delete(*keys)
145
- except Exception as exc:
146
- logger.error(f"Redis删除失败: {keys}, {exc}")
147
- return 0
148
-
149
- async def exists(self, *keys: str) -> int:
150
- """检查缓存是否存在。"""
151
- if not self._redis or not keys:
152
- return 0
153
-
154
- try:
155
- return await self._redis.exists(*keys)
156
- except Exception as exc:
157
- logger.error(f"Redis检查失败: {keys}, {exc}")
158
- return 0
159
-
160
- async def clear(self) -> None:
161
- """清空所有缓存。"""
162
- if self._redis:
163
- await self._redis.flushdb()
164
- logger.info("Redis缓存已清空")
165
-
166
- async def delete_pattern(self, pattern: str) -> int:
167
- """按模式删除缓存。
168
-
169
- Args:
170
- pattern: 通配符模式,如 "todo:*"
171
-
172
- Returns:
173
- int: 删除的键数量
174
- """
175
- if not self._redis:
176
- return 0
177
-
178
- try:
179
- # 使用 SCAN 遍历匹配的键(比 KEYS 更安全,不会阻塞)
180
- count = 0
181
- cursor = 0
182
- while True:
183
- cursor, keys = await self._redis.scan(cursor, match=pattern, count=100)
184
- if keys:
185
- count += await self._redis.delete(*keys)
186
- if cursor == 0:
187
- break
188
- logger.debug(f"按模式删除缓存: {pattern}, 删除 {count} 个键")
189
- return count
190
- except Exception as exc:
191
- logger.error(f"Redis模式删除失败: {pattern}, {exc}")
192
- return 0
193
-
194
- async def close(self) -> None:
195
- """关闭连接(仅当自己拥有连接时)。"""
196
- if self._redis and self._owns_connection:
197
- await self._redis.close()
198
- logger.info("Redis连接已关闭")
199
- self._redis = None
200
-
201
- @property
202
- def redis(self) -> Redis | None:
203
- """获取Redis客户端。"""
204
- return self._redis
205
-
206
-
207
- class MemoryCache(ICache):
208
- """内存缓存实现。"""
209
-
210
- def __init__(self, max_size: int = 1000):
211
- """初始化内存缓存。
212
-
213
- Args:
214
- max_size: 最大缓存项数
215
- """
216
- self._max_size = max_size
217
- self._cache: dict[str, tuple[Any, float | None]] = {}
218
- self._lock = asyncio.Lock()
219
-
220
- async def get(self, key: str, default: Any = None) -> Any:
221
- """获取缓存。"""
222
- async with self._lock:
223
- if key not in self._cache:
224
- return default
225
-
226
- value, expire_at = self._cache[key]
227
-
228
- # 检查过期
229
- if expire_at is not None and asyncio.get_event_loop().time() > expire_at:
230
- del self._cache[key]
231
- return default
232
-
233
- return value
234
-
235
- async def set(
236
- self,
237
- key: str,
238
- value: Any,
239
- expire: int | timedelta | None = None,
240
- ) -> bool:
241
- """设置缓存。"""
242
- async with self._lock:
243
- # 转换过期时间
244
- expire_at = None
245
- if expire:
246
- if isinstance(expire, timedelta):
247
- expire_seconds = expire.total_seconds()
248
- else:
249
- expire_seconds = expire
250
- expire_at = asyncio.get_event_loop().time() + expire_seconds
251
-
252
- # 如果超出容量,删除最旧的
253
- if len(self._cache) >= self._max_size and key not in self._cache:
254
- # 简单策略:删除第一个
255
- first_key = next(iter(self._cache))
256
- del self._cache[first_key]
257
-
258
- self._cache[key] = (value, expire_at)
259
- return True
260
-
261
- async def delete(self, *keys: str) -> int:
262
- """删除缓存。"""
263
- async with self._lock:
264
- count = 0
265
- for key in keys:
266
- if key in self._cache:
267
- del self._cache[key]
268
- count += 1
269
- return count
270
-
271
- async def exists(self, *keys: str) -> int:
272
- """检查缓存是否存在。"""
273
- async with self._lock:
274
- count = 0
275
- for key in keys:
276
- if key in self._cache:
277
- _value, expire_at = self._cache[key]
278
- # 检查是否过期
279
- if expire_at is None or asyncio.get_event_loop().time() <= expire_at:
280
- count += 1
281
- return count
282
-
283
- async def clear(self) -> None:
284
- """清空所有缓存。"""
285
- async with self._lock:
286
- self._cache.clear()
287
- logger.info("内存缓存已清空")
288
-
289
- async def delete_pattern(self, pattern: str) -> int:
290
- """按模式删除缓存。
291
-
292
- Args:
293
- pattern: 通配符模式,支持 * 和 ?
294
-
295
- Returns:
296
- int: 删除的键数量
297
- """
298
- import fnmatch
299
-
300
- async with self._lock:
301
- keys_to_delete = [
302
- key for key in self._cache
303
- if fnmatch.fnmatch(key, pattern)
304
- ]
305
- for key in keys_to_delete:
306
- del self._cache[key]
307
- logger.debug(f"按模式删除缓存: {pattern}, 删除 {len(keys_to_delete)} 个键")
308
- return len(keys_to_delete)
309
-
310
- async def close(self) -> None:
311
- """关闭连接(内存缓存无需关闭)。"""
312
- await self.clear()
313
-
314
- async def size(self) -> int:
315
- """获取缓存大小。"""
316
- return len(self._cache)
317
-
318
-
319
- class MemcachedCache(ICache):
320
- """Memcached缓存实现(可选)。"""
321
-
322
- def __init__(self, servers: list[str]):
323
- """初始化Memcached缓存。
324
-
325
- Args:
326
- servers: Memcached服务器列表,如 ["127.0.0.1:11211"]
327
- """
328
- self._servers = servers
329
- self._client = None
330
-
331
- async def initialize(self) -> None:
332
- """初始化连接。"""
333
- try:
334
- # 需要安装 python-memcached 或 aiomcache
335
- try:
336
- import aiomcache
337
- self._client = aiomcache.Client(
338
- self._servers[0].split(":")[0],
339
- int(self._servers[0].split(":")[1]) if ":" in self._servers[0] else 11211,
340
- )
341
- logger.info("Memcached缓存初始化成功")
342
- except ImportError:
343
- logger.error("请安装 aiomcache: pip install aiomcache")
344
- raise
345
- except Exception as exc:
346
- logger.error(f"Memcached连接失败: {exc}")
347
- raise
348
-
349
- async def get(self, key: str, default: Any = None) -> Any:
350
- """获取缓存。"""
351
- if not self._client:
352
- return default
353
-
354
- try:
355
- data = await self._client.get(key.encode())
356
- if data is None:
357
- return default
358
- return json.loads(data.decode())
359
- except Exception as exc:
360
- logger.error(f"Memcached获取失败: {key}, {exc}")
361
- return default
362
-
363
- async def set(
364
- self,
365
- key: str,
366
- value: Any,
367
- expire: int | timedelta | None = None,
368
- ) -> bool:
369
- """设置缓存。"""
370
- if not self._client:
371
- return False
372
-
373
- try:
374
- if isinstance(expire, timedelta):
375
- expire = int(expire.total_seconds())
376
-
377
- data = json.dumps(value).encode()
378
- return await self._client.set(key.encode(), data, exptime=expire or 0)
379
- except Exception as exc:
380
- logger.error(f"Memcached设置失败: {key}, {exc}")
381
- return False
382
-
383
- async def delete(self, *keys: str) -> int:
384
- """删除缓存。"""
385
- if not self._client or not keys:
386
- return 0
387
-
388
- count = 0
389
- for key in keys:
390
- try:
391
- if await self._client.delete(key.encode()):
392
- count += 1
393
- except Exception as exc:
394
- logger.error(f"Memcached删除失败: {key}, {exc}")
395
- return count
396
-
397
- async def exists(self, *keys: str) -> int:
398
- """检查缓存是否存在。"""
399
- if not self._client or not keys:
400
- return 0
401
-
402
- count = 0
403
- for key in keys:
404
- try:
405
- if await self._client.get(key.encode()) is not None:
406
- count += 1
407
- except Exception:
408
- pass
409
- return count
410
-
411
- async def clear(self) -> None:
412
- """清空所有缓存(Memcached不支持)。"""
413
- logger.warning("Memcached不支持清空所有缓存")
414
-
415
- async def delete_pattern(self, pattern: str) -> int:
416
- """按模式删除缓存(Memcached 不支持)。"""
417
- logger.warning("Memcached 不支持模式删除,请使用 Redis 或 Memory 后端")
418
- return 0
419
-
420
- async def close(self) -> None:
421
- """关闭连接。"""
422
- if self._client:
423
- self._client.close()
424
- logger.info("Memcached连接已关闭")
425
-
6
+ from .memory import MemcachedCache, MemoryCache
7
+ from .redis import RedisCache
426
8
 
427
9
  __all__ = [
428
10
  "MemcachedCache",
@@ -71,6 +71,44 @@ class ICache(ABC):
71
71
  async def close(self) -> None:
72
72
  """关闭连接。"""
73
73
  pass
74
+
75
+ # ==================== 分布式锁 ====================
76
+
77
+ @abstractmethod
78
+ async def acquire_lock(
79
+ self,
80
+ key: str,
81
+ token: str,
82
+ timeout: int,
83
+ blocking: bool,
84
+ blocking_timeout: float | None,
85
+ ) -> bool:
86
+ """获取分布式锁。
87
+
88
+ Args:
89
+ key: 锁的键名(已加 lock: 前缀)
90
+ token: 锁的 token
91
+ timeout: 锁的超时时间(秒)
92
+ blocking: 是否阻塞等待
93
+ blocking_timeout: 阻塞等待的最大时间(秒)
94
+
95
+ Returns:
96
+ bool: 是否获取成功
97
+ """
98
+ pass
99
+
100
+ @abstractmethod
101
+ async def release_lock(self, key: str, token: str) -> bool:
102
+ """释放分布式锁。
103
+
104
+ Args:
105
+ key: 锁的键名(已加 lock: 前缀)
106
+ token: 获取锁时的 token
107
+
108
+ Returns:
109
+ bool: 是否成功释放
110
+ """
111
+ pass
74
112
 
75
113
 
76
114
  __all__ = [
@@ -5,11 +5,15 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import asyncio
8
9
  from collections.abc import Callable
10
+ from contextlib import asynccontextmanager
9
11
  from datetime import timedelta
10
12
  from functools import wraps
11
13
  import hashlib
12
- from typing import Any, TypeVar
14
+ import time
15
+ from typing import Any, AsyncIterator, TypeVar
16
+ import uuid
13
17
 
14
18
  from aury.boot.common.logging import logger
15
19
 
@@ -328,6 +332,152 @@ class CacheManager:
328
332
  self._backend = None
329
333
  logger.info("缓存管理器已清理")
330
334
 
335
+ # ==================== 分布式锁 ====================
336
+
337
+ async def acquire_lock(
338
+ self,
339
+ key: str,
340
+ *,
341
+ timeout: int = 30,
342
+ blocking: bool = True,
343
+ blocking_timeout: float | None = None,
344
+ ) -> str | None:
345
+ """获取分布式锁。
346
+
347
+ Args:
348
+ key: 锁的键名
349
+ timeout: 锁的超时时间(秒),防止死锁
350
+ blocking: 是否阻塞等待
351
+ blocking_timeout: 阻塞等待的最大时间(秒)
352
+
353
+ Returns:
354
+ str | None: 锁的 token(用于释放),获取失败返回 None
355
+ """
356
+ lock_key = f"lock:{key}"
357
+ token = str(uuid.uuid4())
358
+
359
+ acquired = await self.backend.acquire_lock(
360
+ lock_key, token, timeout, blocking, blocking_timeout
361
+ )
362
+ return token if acquired else None
363
+
364
+ async def release_lock(self, key: str, token: str) -> bool:
365
+ """释放分布式锁。
366
+
367
+ Args:
368
+ key: 锁的键名
369
+ token: acquire_lock 返回的 token
370
+
371
+ Returns:
372
+ bool: 是否成功释放
373
+ """
374
+ lock_key = f"lock:{key}"
375
+ return await self.backend.release_lock(lock_key, token)
376
+
377
+ @asynccontextmanager
378
+ async def lock(
379
+ self,
380
+ key: str,
381
+ *,
382
+ timeout: int = 30,
383
+ blocking: bool = True,
384
+ blocking_timeout: float | None = None,
385
+ ) -> AsyncIterator[bool]:
386
+ """分布式锁上下文管理器。
387
+
388
+ Args:
389
+ key: 锁的键名
390
+ timeout: 锁的超时时间(秒)
391
+ blocking: 是否阻塞等待
392
+ blocking_timeout: 阻塞等待的最大时间(秒)
393
+
394
+ Yields:
395
+ bool: 是否成功获取锁
396
+
397
+ 示例:
398
+ async with cache.lock("my_resource") as acquired:
399
+ if acquired:
400
+ # 执行需要互斥的操作
401
+ pass
402
+ """
403
+ token = await self.acquire_lock(
404
+ key,
405
+ timeout=timeout,
406
+ blocking=blocking,
407
+ blocking_timeout=blocking_timeout,
408
+ )
409
+ try:
410
+ yield token is not None
411
+ finally:
412
+ if token:
413
+ await self.release_lock(key, token)
414
+
415
+ @asynccontextmanager
416
+ async def semaphore(
417
+ self,
418
+ key: str,
419
+ max_concurrency: int,
420
+ *,
421
+ timeout: int = 300,
422
+ blocking: bool = True,
423
+ blocking_timeout: float | None = None,
424
+ ) -> AsyncIterator[bool]:
425
+ """分布式信号量(限制并发数)。
426
+
427
+ Args:
428
+ key: 信号量的键名
429
+ max_concurrency: 最大并发数
430
+ timeout: 单个槽位的超时时间(秒)
431
+ blocking: 是否阻塞等待
432
+ blocking_timeout: 阻塞等待的最大时间(秒)
433
+
434
+ Yields:
435
+ bool: 是否成功获取槽位
436
+
437
+ 示例:
438
+ async with cache.semaphore("pdf_ocr", max_concurrency=2) as acquired:
439
+ if acquired:
440
+ # 执行受并发限制的操作
441
+ pass
442
+ """
443
+ slot_token: str | None = None
444
+ acquired_slot: int | None = None
445
+ start_time = time.monotonic()
446
+
447
+ try:
448
+ while True:
449
+ # 尝试获取任意一个槽位
450
+ for slot in range(max_concurrency):
451
+ slot_key = f"{key}:slot:{slot}"
452
+ token = await self.acquire_lock(
453
+ slot_key,
454
+ timeout=timeout,
455
+ blocking=False,
456
+ )
457
+ if token:
458
+ slot_token = token
459
+ acquired_slot = slot
460
+ yield True
461
+ return
462
+
463
+ if not blocking:
464
+ yield False
465
+ return
466
+
467
+ # 检查是否超时
468
+ if blocking_timeout is not None:
469
+ elapsed = time.monotonic() - start_time
470
+ if elapsed >= blocking_timeout:
471
+ yield False
472
+ return
473
+
474
+ # 等待后重试
475
+ await asyncio.sleep(0.1)
476
+ finally:
477
+ if slot_token and acquired_slot is not None:
478
+ slot_key = f"{key}:slot:{acquired_slot}"
479
+ await self.release_lock(slot_key, slot_token)
480
+
331
481
  def __repr__(self) -> str:
332
482
  """字符串表示。"""
333
483
  backend_name = self.backend_type if self._backend else "未初始化"