blitz_api/app/api/task_utils.py
2025-05-06 09:16:46 +02:00

91 lines
3 KiB
Python

from loguru import logger
from redis.asyncio import Redis
from app.api.error_report.report import Frame, Report
from app.api.utils import redis_delete, redis_get_raw, redis_set
from app.apps.constants import DB_LOCKED_KEY
from app.external.result_type.src.result.result import Err, Ok, Result
async def get_lock_status(key: str, redis: Redis | None = None) -> Result[bool, Report]:
"""
Checks if the update lock is held.
Returns True if lock is held.
"""
logger.trace(f"get_lock_status({key})")
result = await redis_get_raw(key, custom_redis=redis)
match result:
case Ok(None):
logger.debug(f"Lock for key {key} not held.")
return Ok(False)
# data can either be a str or a bytes object depending on whether the Redis
# client is configured to decode responses or not
case Ok(data) if (isinstance(data, str) and data == DB_LOCKED_KEY) or (
isinstance(data, bytes) and data.decode("utf-8") == DB_LOCKED_KEY
):
logger.debug(f"Lock held for key {key}.")
return Ok(True)
case Ok(data) if not isinstance(data, bytes):
return Err(Report(message=f"Unexpected data from Redis: {data}"))
case Err(e):
logger.error(f"Error checking if lock is held for key {key}: %s", e)
return Err(e)
return Err(Report(message=f"Error checking if lock is held for key {key}"))
async def acquire_lock(
key: str, lock_ttl: int, redis: Redis | None = None
) -> Result[bool, Report]:
"""
Attempts to acquire a lock to prevent concurrent updates.
Returns True if lock acquired.
"""
logger.trace(f"acquire_lock({key})")
result = await get_lock_status(key, redis)
match result:
case Ok(lock_status):
if lock_status:
return Ok(False)
case Err(report):
return Err(report)
# nx: Only set the key if it does not already exist.
# ex: Set the specified expire time, in seconds.
# TODO: should this even timeout automatically?
# do we need to do some cleanup if the status update takes too long?
match await redis_set(
key,
DB_LOCKED_KEY,
nx=True,
ex=lock_ttl,
custom_redis=redis,
):
case Ok(_):
return Ok(True)
case Err(report):
return Err(
report.attach_frame(
Frame(message="Error acquiring update lock in Redis")
)
)
return Err(Report(message="Error acquiring update lock in Redis"))
async def release_update_lock(
key: str, redis: Redis | None = None
) -> Result[None, Report]:
"""Releases the update lock."""
logger.trace(f"release_update_lock({key})")
match await redis_delete(key, custom_redis=redis):
case Ok(_):
return Ok(None)
case Err(report):
return Err(
report.attach_frame(
Frame(message=f"Error releasing lock in Redis for {key}")
)
)