atlan-application-sdk 0.1.1rc34__py3-none-any.whl → 0.1.1rc36__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.
- application_sdk/activities/__init__.py +3 -2
- application_sdk/activities/common/utils.py +21 -1
- application_sdk/activities/lock_management.py +110 -0
- application_sdk/activities/metadata_extraction/base.py +4 -2
- application_sdk/activities/metadata_extraction/sql.py +13 -12
- application_sdk/activities/query_extraction/sql.py +24 -20
- application_sdk/clients/atlan_auth.py +2 -2
- application_sdk/clients/redis.py +443 -0
- application_sdk/clients/temporal.py +36 -196
- application_sdk/common/error_codes.py +24 -3
- application_sdk/constants.py +18 -1
- application_sdk/decorators/__init__.py +0 -0
- application_sdk/decorators/locks.py +42 -0
- application_sdk/handlers/base.py +18 -1
- application_sdk/inputs/json.py +6 -4
- application_sdk/inputs/parquet.py +16 -13
- application_sdk/interceptors/__init__.py +0 -0
- application_sdk/interceptors/events.py +193 -0
- application_sdk/interceptors/lock.py +139 -0
- application_sdk/outputs/__init__.py +6 -3
- application_sdk/outputs/json.py +9 -6
- application_sdk/outputs/parquet.py +10 -36
- application_sdk/server/fastapi/__init__.py +4 -5
- application_sdk/services/__init__.py +18 -0
- application_sdk/{outputs → services}/atlan_storage.py +64 -16
- application_sdk/{outputs → services}/eventstore.py +68 -6
- application_sdk/services/objectstore.py +407 -0
- application_sdk/services/secretstore.py +344 -0
- application_sdk/services/statestore.py +267 -0
- application_sdk/version.py +1 -1
- application_sdk/worker.py +1 -1
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/METADATA +4 -2
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/RECORD +36 -32
- application_sdk/common/credential_utils.py +0 -85
- application_sdk/inputs/objectstore.py +0 -238
- application_sdk/inputs/secretstore.py +0 -130
- application_sdk/inputs/statestore.py +0 -101
- application_sdk/outputs/objectstore.py +0 -125
- application_sdk/outputs/secretstore.py +0 -38
- application_sdk/outputs/statestore.py +0 -113
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-0.1.1rc34.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Redis client for distributed locking with high availability support."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import NoReturn, Union
|
|
5
|
+
|
|
6
|
+
import redis
|
|
7
|
+
import redis.asyncio as async_redis
|
|
8
|
+
from redis.exceptions import ConnectionError, RedisError, TimeoutError
|
|
9
|
+
|
|
10
|
+
from application_sdk.common.error_codes import ClientError
|
|
11
|
+
from application_sdk.constants import (
|
|
12
|
+
IS_LOCKING_DISABLED,
|
|
13
|
+
REDIS_HOST,
|
|
14
|
+
REDIS_PASSWORD,
|
|
15
|
+
REDIS_PORT,
|
|
16
|
+
REDIS_SENTINEL_HOSTS,
|
|
17
|
+
REDIS_SENTINEL_SERVICE_NAME,
|
|
18
|
+
)
|
|
19
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _handle_redis_error(e: Exception) -> NoReturn:
|
|
25
|
+
"""Handle Redis errors with consistent error mapping.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
e: The Redis exception that occurred
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ClientError: Appropriate ClientError based on exception type
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(e, ConnectionError):
|
|
34
|
+
raise ClientError(f"{ClientError.REDIS_CONNECTION_ERROR}: {e}")
|
|
35
|
+
elif isinstance(e, TimeoutError):
|
|
36
|
+
raise ClientError(f"{ClientError.REDIS_TIMEOUT_ERROR}: {e}")
|
|
37
|
+
elif isinstance(e, RedisError):
|
|
38
|
+
raise ClientError(f"{ClientError.REDIS_PROTOCOL_ERROR}: {e}")
|
|
39
|
+
else:
|
|
40
|
+
raise ClientError(f"{ClientError.REDIS_CONNECTION_ERROR}: {e}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LockReleaseResult(Enum):
|
|
44
|
+
"""Enum for lock release operation results."""
|
|
45
|
+
|
|
46
|
+
SUCCESS = "success"
|
|
47
|
+
ALREADY_RELEASED = "already_released"
|
|
48
|
+
WRONG_OWNER = "wrong_owner"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_LOCK_RELEASE_LUA_SCRIPT = """
|
|
52
|
+
local current_owner = redis.call("GET", KEYS[1])
|
|
53
|
+
if current_owner == false then
|
|
54
|
+
return -1 -- Key doesn't exist
|
|
55
|
+
elseif current_owner ~= ARGV[1] then
|
|
56
|
+
return -2 -- Wrong owner
|
|
57
|
+
else
|
|
58
|
+
return redis.call("DEL", KEYS[1]) -- Success (returns 1)
|
|
59
|
+
end
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BaseRedisClient:
|
|
64
|
+
"""Base Redis client with common functionality."""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
"""Initialize Redis client configuration."""
|
|
68
|
+
if IS_LOCKING_DISABLED:
|
|
69
|
+
logger.info("Strict locking disabled - skipping Redis connection")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Validate Redis configuration
|
|
73
|
+
if not REDIS_PASSWORD or (
|
|
74
|
+
not REDIS_SENTINEL_HOSTS and not (REDIS_HOST and REDIS_PORT)
|
|
75
|
+
):
|
|
76
|
+
logger.error(
|
|
77
|
+
"Redis configuration invalid: REDIS_PASSWORD is required and either REDIS_SENTINEL_HOSTS or REDIS_HOST/REDIS_PORT must be configured"
|
|
78
|
+
)
|
|
79
|
+
raise ClientError(
|
|
80
|
+
f"{ClientError.REQUEST_VALIDATION_ERROR}: Redis configuration invalid - REDIS_PASSWORD is required and either REDIS_SENTINEL_HOSTS or REDIS_HOST/REDIS_PORT must be configured"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _parse_sentinel_hosts(self) -> list[tuple[str, int]]:
|
|
84
|
+
"""Parse sentinel hosts from configuration.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of (host, port) tuples
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If host format is invalid
|
|
91
|
+
ClientError: If no hosts are configured
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
sentinel_hosts = [
|
|
95
|
+
(host.strip(), int(port))
|
|
96
|
+
for host_port in REDIS_SENTINEL_HOSTS.split(",")
|
|
97
|
+
for host, port in [host_port.strip().rsplit(":", 1)]
|
|
98
|
+
]
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
logger.error(
|
|
101
|
+
f"Invalid Sentinel host format in REDIS_SENTINEL_HOSTS '{REDIS_SENTINEL_HOSTS}': {e}"
|
|
102
|
+
)
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
if not sentinel_hosts:
|
|
106
|
+
logger.error("No Sentinel hosts configured")
|
|
107
|
+
raise ClientError(
|
|
108
|
+
f"{ClientError.REQUEST_VALIDATION_ERROR}: No Sentinel hosts configured"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return sentinel_hosts
|
|
112
|
+
|
|
113
|
+
def _process_lock_release_result(
|
|
114
|
+
self, result: Union[int, None], resource_id: str
|
|
115
|
+
) -> tuple[bool, LockReleaseResult]:
|
|
116
|
+
"""Process lock release Lua script result.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
result (int | None): Result from Redis eval command.
|
|
120
|
+
resource_id (str): Resource ID for logging.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
tuple[bool, LockReleaseResult]: A tuple ``(success, outcome)``.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ClientError: If result type is unexpected or unknown.
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(result, int):
|
|
129
|
+
logger.error(
|
|
130
|
+
f"Unexpected eval result type for {resource_id}: {type(result)}, value: {result}"
|
|
131
|
+
)
|
|
132
|
+
raise ClientError(
|
|
133
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if result >= 1:
|
|
137
|
+
return True, LockReleaseResult.SUCCESS
|
|
138
|
+
elif result == -1:
|
|
139
|
+
return (
|
|
140
|
+
True,
|
|
141
|
+
LockReleaseResult.ALREADY_RELEASED,
|
|
142
|
+
) # Not an error - TTL expired
|
|
143
|
+
elif result == -2:
|
|
144
|
+
return False, LockReleaseResult.WRONG_OWNER
|
|
145
|
+
else:
|
|
146
|
+
logger.error(f"Unknown Redis eval result for {resource_id}: {result}")
|
|
147
|
+
raise ClientError(
|
|
148
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class RedisClient(BaseRedisClient):
|
|
153
|
+
"""Synchronous Redis client for distributed locking."""
|
|
154
|
+
|
|
155
|
+
def __init__(self):
|
|
156
|
+
"""Initialize sync Redis client."""
|
|
157
|
+
super().__init__()
|
|
158
|
+
self.redis_client = None
|
|
159
|
+
|
|
160
|
+
def _connect(self) -> None:
|
|
161
|
+
"""Establish sync Redis connection."""
|
|
162
|
+
if IS_LOCKING_DISABLED:
|
|
163
|
+
logger.info("Locking disabled - Redis client will operate in no-op mode")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
if REDIS_SENTINEL_HOSTS:
|
|
168
|
+
self._connect_via_sentinel()
|
|
169
|
+
else:
|
|
170
|
+
self._connect_standalone()
|
|
171
|
+
|
|
172
|
+
# Test connection
|
|
173
|
+
if not self.redis_client:
|
|
174
|
+
raise ClientError(
|
|
175
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.redis_client.ping()
|
|
179
|
+
logger.info("Sync Redis connection established for strict locking")
|
|
180
|
+
|
|
181
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
182
|
+
_handle_redis_error(e)
|
|
183
|
+
|
|
184
|
+
def _connect_via_sentinel(self) -> None:
|
|
185
|
+
"""Connect to Redis via Sentinel using sync client."""
|
|
186
|
+
sentinel_hosts = self._parse_sentinel_hosts()
|
|
187
|
+
logger.info(f"Connecting to Redis via sync Sentinel: {sentinel_hosts}")
|
|
188
|
+
logger.info(f"Service name: {REDIS_SENTINEL_SERVICE_NAME}")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Create Sentinel with password
|
|
192
|
+
sentinel = redis.sentinel.Sentinel(
|
|
193
|
+
sentinel_hosts, sentinel_kwargs={"password": REDIS_PASSWORD}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Create master client with password
|
|
197
|
+
self.redis_client = sentinel.master_for(
|
|
198
|
+
REDIS_SENTINEL_SERVICE_NAME, password=REDIS_PASSWORD
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
202
|
+
_handle_redis_error(e)
|
|
203
|
+
|
|
204
|
+
def _connect_standalone(self) -> None:
|
|
205
|
+
"""Connect to standalone Redis instance using sync client."""
|
|
206
|
+
logger.debug(f"Connecting to standalone sync Redis: {REDIS_HOST}:{REDIS_PORT}")
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
self.redis_client = redis.Redis(
|
|
210
|
+
host=REDIS_HOST, port=int(REDIS_PORT), password=REDIS_PASSWORD
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
214
|
+
_handle_redis_error(e)
|
|
215
|
+
|
|
216
|
+
def close(self) -> None:
|
|
217
|
+
"""Close the sync Redis client and clean up resources."""
|
|
218
|
+
if self.redis_client:
|
|
219
|
+
try:
|
|
220
|
+
self.redis_client.close()
|
|
221
|
+
logger.info("Sync Redis connection closed")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"Error closing sync Redis connection: {e}")
|
|
224
|
+
finally:
|
|
225
|
+
self.redis_client = None
|
|
226
|
+
|
|
227
|
+
def __enter__(self):
|
|
228
|
+
"""Sync context manager entry."""
|
|
229
|
+
self._connect()
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
233
|
+
"""Sync context manager exit with guaranteed cleanup."""
|
|
234
|
+
self.close()
|
|
235
|
+
|
|
236
|
+
def _acquire_lock(
|
|
237
|
+
self, resource_id: str, owner_id: str = "default_owner", ttl_seconds: int = 100
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""Synchronously acquire a distributed lock.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
resource_id: Unique identifier for the resource to lock
|
|
243
|
+
owner_id: Identifier for the lock owner
|
|
244
|
+
ttl_seconds: Time-to-live for the lock in seconds
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if lock was acquired, False if lock is already held by another owner
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ClientError: If Redis connection or operation fails
|
|
251
|
+
"""
|
|
252
|
+
if not self.redis_client:
|
|
253
|
+
logger.error("Sync Redis client not initialized")
|
|
254
|
+
raise ClientError(
|
|
255
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
result = self.redis_client.set(
|
|
260
|
+
resource_id, owner_id, nx=True, ex=ttl_seconds
|
|
261
|
+
)
|
|
262
|
+
return bool(result)
|
|
263
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
264
|
+
_handle_redis_error(e)
|
|
265
|
+
|
|
266
|
+
def _release_lock(
|
|
267
|
+
self, resource_id: str, owner_id: str = "default_owner"
|
|
268
|
+
) -> tuple[bool, LockReleaseResult]:
|
|
269
|
+
"""Synchronously release a lock with ownership verification.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
resource_id (str): Unique identifier for the resource to unlock.
|
|
273
|
+
owner_id (str): Identifier for the lock owner.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
tuple[bool, LockReleaseResult]: Result of the release operation.
|
|
277
|
+
- (True, LockReleaseResult.SUCCESS): Lock released successfully.
|
|
278
|
+
- (True, LockReleaseResult.ALREADY_RELEASED): Lock was already released (TTL expired).
|
|
279
|
+
- (False, LockReleaseResult.WRONG_OWNER): Lock owned by a different owner.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ClientError: If Redis connection or operation fails.
|
|
283
|
+
"""
|
|
284
|
+
if not self.redis_client:
|
|
285
|
+
logger.error("Sync Redis client not initialized")
|
|
286
|
+
raise ClientError(
|
|
287
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
result = self.redis_client.eval(
|
|
292
|
+
_LOCK_RELEASE_LUA_SCRIPT, 1, resource_id, owner_id
|
|
293
|
+
)
|
|
294
|
+
return self._process_lock_release_result(result, resource_id)
|
|
295
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
296
|
+
_handle_redis_error(e)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class RedisClientAsync(BaseRedisClient):
|
|
300
|
+
"""Asynchronous Redis client for distributed locking."""
|
|
301
|
+
|
|
302
|
+
def __init__(self):
|
|
303
|
+
"""Initialize async Redis client."""
|
|
304
|
+
super().__init__()
|
|
305
|
+
self.redis_client = None
|
|
306
|
+
|
|
307
|
+
async def _connect(self) -> None:
|
|
308
|
+
"""Establish async Redis connection."""
|
|
309
|
+
if IS_LOCKING_DISABLED:
|
|
310
|
+
logger.info("Locking disabled - Redis client will operate in no-op mode")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
if REDIS_SENTINEL_HOSTS:
|
|
315
|
+
await self._connect_via_sentinel()
|
|
316
|
+
else:
|
|
317
|
+
await self._connect_standalone()
|
|
318
|
+
|
|
319
|
+
# Test connection
|
|
320
|
+
if not self.redis_client:
|
|
321
|
+
raise ClientError(
|
|
322
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
await self.redis_client.ping()
|
|
326
|
+
logger.info("Async Redis connection established for strict locking")
|
|
327
|
+
|
|
328
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
329
|
+
_handle_redis_error(e)
|
|
330
|
+
|
|
331
|
+
async def _connect_via_sentinel(self) -> None:
|
|
332
|
+
"""Connect to Redis via Sentinel using async client."""
|
|
333
|
+
sentinel_hosts = self._parse_sentinel_hosts()
|
|
334
|
+
logger.info(f"Connecting to Redis via async Sentinel: {sentinel_hosts}")
|
|
335
|
+
logger.info(f"Service name: {REDIS_SENTINEL_SERVICE_NAME}")
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
# Create Sentinel with password
|
|
339
|
+
sentinel = async_redis.sentinel.Sentinel(
|
|
340
|
+
sentinel_hosts, sentinel_kwargs={"password": REDIS_PASSWORD}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Create master client with password
|
|
344
|
+
self.redis_client = sentinel.master_for(
|
|
345
|
+
REDIS_SENTINEL_SERVICE_NAME, password=REDIS_PASSWORD
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
349
|
+
_handle_redis_error(e)
|
|
350
|
+
|
|
351
|
+
async def _connect_standalone(self) -> None:
|
|
352
|
+
"""Connect to standalone Redis instance using async client."""
|
|
353
|
+
logger.debug(f"Connecting to standalone async Redis: {REDIS_HOST}:{REDIS_PORT}")
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
self.redis_client = async_redis.Redis(
|
|
357
|
+
host=REDIS_HOST, port=int(REDIS_PORT), password=REDIS_PASSWORD
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
361
|
+
_handle_redis_error(e)
|
|
362
|
+
|
|
363
|
+
async def close(self) -> None:
|
|
364
|
+
"""Close the Redis client and clean up resources."""
|
|
365
|
+
if self.redis_client:
|
|
366
|
+
try:
|
|
367
|
+
await self.redis_client.aclose()
|
|
368
|
+
logger.info("Async Redis connection closed")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error(f"Error closing async Redis connection: {e}")
|
|
371
|
+
finally:
|
|
372
|
+
self.redis_client = None
|
|
373
|
+
|
|
374
|
+
async def __aenter__(self):
|
|
375
|
+
"""Async context manager entry."""
|
|
376
|
+
await self._connect()
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
380
|
+
"""Async context manager exit with guaranteed cleanup."""
|
|
381
|
+
await self.close()
|
|
382
|
+
|
|
383
|
+
async def _acquire_lock(
|
|
384
|
+
self, resource_id: str, owner_id: str = "default_owner", ttl_seconds: int = 100
|
|
385
|
+
) -> bool:
|
|
386
|
+
"""Asynchronously acquire a distributed lock.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
resource_id: Unique identifier for the resource to lock
|
|
390
|
+
owner_id: Identifier for the lock owner
|
|
391
|
+
ttl_seconds: Time-to-live for the lock in seconds
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
True if lock was acquired, False if lock is already held by another owner
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ClientError: If Redis connection or operation fails
|
|
398
|
+
"""
|
|
399
|
+
if not self.redis_client:
|
|
400
|
+
logger.error("Redis client not initialized")
|
|
401
|
+
raise ClientError(
|
|
402
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
result = await self.redis_client.set(
|
|
407
|
+
resource_id, owner_id, nx=True, ex=ttl_seconds
|
|
408
|
+
)
|
|
409
|
+
return bool(result)
|
|
410
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
411
|
+
_handle_redis_error(e)
|
|
412
|
+
|
|
413
|
+
async def _release_lock(
|
|
414
|
+
self, resource_id: str, owner_id: str = "default_owner"
|
|
415
|
+
) -> tuple[bool, LockReleaseResult]:
|
|
416
|
+
"""Asynchronously release a lock with ownership verification.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
resource_id (str): Unique identifier for the resource to unlock.
|
|
420
|
+
owner_id (str): Identifier for the lock owner.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
tuple[bool, LockReleaseResult]: Result of the release operation.
|
|
424
|
+
- (True, LockReleaseResult.SUCCESS): Lock released successfully.
|
|
425
|
+
- (True, LockReleaseResult.ALREADY_RELEASED): Lock was already released (TTL expired).
|
|
426
|
+
- (False, LockReleaseResult.WRONG_OWNER): Lock owned by a different owner.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ClientError: If Redis connection or operation fails.
|
|
430
|
+
"""
|
|
431
|
+
if not self.redis_client:
|
|
432
|
+
logger.error("Redis client not initialized")
|
|
433
|
+
raise ClientError(
|
|
434
|
+
f"{ClientError.REDIS_CONNECTION_ERROR}: Redis connection failed"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
result = await self.redis_client.eval(
|
|
439
|
+
_LOCK_RELEASE_LUA_SCRIPT, 1, resource_id, owner_id
|
|
440
|
+
)
|
|
441
|
+
return self._process_lock_release_result(result, resource_id)
|
|
442
|
+
except (ConnectionError, TimeoutError, RedisError, Exception) as e:
|
|
443
|
+
_handle_redis_error(e)
|