mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-16 12:20:14 +02:00
260 lines
9.4 KiB
Python
260 lines
9.4 KiB
Python
import json
|
|
from typing import Optional
|
|
|
|
from loguru import logger
|
|
from redis.asyncio import Redis
|
|
|
|
from app.api.channel import BaseChannelListener
|
|
from app.api.config import config
|
|
from app.api.error_report.report import Report
|
|
from app.api.models import ErrorMessage
|
|
from app.api.utils import SSE, broadcast_sse_msg, redis_get, redis_get_raw, redis_set
|
|
from app.apps.constants import AppsServiceActions, AppsServiceKeys
|
|
from app.apps.models import AppStatusQueryResult
|
|
from app.external.result_type.src.result.result import Err, Ok, Result
|
|
from app.logging import configure_logger
|
|
|
|
CACHE_TTL_SECONDS = int(config("BAPI_CACHE_TTL_SECONDS", default=35 * 60))
|
|
if not isinstance(CACHE_TTL_SECONDS, int):
|
|
raise TypeError("BAPI_CACHE_TTL_SECONDS must be an integer")
|
|
|
|
LOCK_TTL_SECONDS = int(config("BAPI_LOCK_TTL_SECONDS", default=5 * 60))
|
|
if not isinstance(LOCK_TTL_SECONDS, int):
|
|
raise TypeError("BAPI_LOCK_TTL_SECONDS must be an integer")
|
|
|
|
configure_logger()
|
|
|
|
|
|
BAPI_REDIS_URL = config("BAPI_REDIS_URL", "redis://127.0.0.1:6379/0")
|
|
if BAPI_REDIS_URL == "":
|
|
raise Exception("BAPI_REDIS_URL is not set")
|
|
|
|
|
|
class _AppsChannelListener(BaseChannelListener):
|
|
def __init__(self):
|
|
super().__init__(AppsServiceKeys.APP_STATE_CHANNEL, BAPI_REDIS_URL)
|
|
|
|
async def handle_event(self, event):
|
|
"""Process app state change events from the Redis channel"""
|
|
logger.debug(f"Received app state event: {event}")
|
|
|
|
key = action = new_value = None
|
|
try:
|
|
key = event.get("key")
|
|
action = event.get("action")
|
|
new_value = event.get("new_value")
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse channel event JSON data: {e}")
|
|
except KeyError as e:
|
|
logger.error(f"Missing required key in channel event: {e}")
|
|
except Exception as e:
|
|
logger.exception(f"Unexpected error handling channel event: {e}")
|
|
|
|
if not key or not action:
|
|
logger.error("Missing key or action in channel event")
|
|
return
|
|
|
|
match (key, action):
|
|
case (AppsServiceKeys.APP_STATUS_CACHE_KEY, AppsServiceActions.STARTED):
|
|
await broadcast_sse_msg(SSE.APP_STATE_UPDATING, None)
|
|
case (AppsServiceKeys.APP_STATUS_CACHE_KEY, AppsServiceActions.UPDATED):
|
|
try:
|
|
if new_value:
|
|
parsed_status = json.loads(new_value)
|
|
await broadcast_sse_msg(SSE.INSTALLED_APP_STATUS, parsed_status)
|
|
await broadcast_sse_msg(SSE.APP_STATE_UPDATING_SUCCESS, None)
|
|
logger.info(
|
|
"App state updated via channel and broadcasted to clients"
|
|
)
|
|
else:
|
|
logger.warning("Received app status update with no value")
|
|
except Exception as e:
|
|
logger.error(f"Failed to process app status update: {e}")
|
|
case (
|
|
AppsServiceKeys.APP_STATUS_UPDATE_FAILED_KEY,
|
|
AppsServiceActions.ERROR,
|
|
):
|
|
try:
|
|
if new_value:
|
|
error_payload = json.loads(new_value)
|
|
await broadcast_sse_msg(
|
|
SSE.APP_STATE_UPDATE_ERROR, error_payload
|
|
)
|
|
logger.info("App state update error broadcasted to clients")
|
|
else:
|
|
logger.warning("Received app status error with no value")
|
|
except Exception as e:
|
|
logger.error(f"Failed to process app status error: {e}")
|
|
case (AppsServiceKeys.APP_STATUS_LOCK_KEY, AppsServiceActions.LOCKED):
|
|
logger.debug(f"Received lock status change: {action}")
|
|
case _:
|
|
logger.warning(
|
|
f"Received unknown channel event - key: {key}, action: {action}"
|
|
)
|
|
|
|
|
|
async def watch_app_status_changes() -> Result[None, Report]:
|
|
"""
|
|
Listens for app status changes on the custom Redis channel.
|
|
This function implements the channel listener for app state changes.
|
|
"""
|
|
|
|
logger.info("Starting app status channel listener")
|
|
|
|
try:
|
|
listener = _AppsChannelListener()
|
|
await listener.connect()
|
|
# This will loop forever
|
|
await listener.listen()
|
|
|
|
except Exception as e:
|
|
return Err(Report(f"App status channel listener error: {e}", error=e))
|
|
|
|
logger.info("App status channel listener stopped")
|
|
return Ok(None)
|
|
|
|
|
|
async def get_cached_app_status(
|
|
redis: Redis | None = None,
|
|
) -> Result[Optional[AppStatusQueryResult], Report]:
|
|
"""Retrieves the cached app status from Redis."""
|
|
logger.trace("get_cached_app_status()")
|
|
try:
|
|
result = await redis_get_raw(
|
|
AppsServiceKeys.APP_STATUS_CACHE_KEY, custom_redis=redis
|
|
)
|
|
match result:
|
|
case Ok(None):
|
|
logger.debug("App status cache not hit.")
|
|
return Ok(None)
|
|
case Ok(data) if isinstance(data, str) and data:
|
|
logger.debug("App status cache hit.")
|
|
data = AppStatusQueryResult.model_validate_json(data)
|
|
case Ok(data):
|
|
logger.error(f"Error decoding app status from Redis cache: {data}")
|
|
return Err(
|
|
Report(f"Error decoding app status from Redis cache: {data}")
|
|
)
|
|
case Err(e):
|
|
logger.error(f"Error getting app status from Redis cache: {e}")
|
|
return Err(e)
|
|
|
|
return Ok(data)
|
|
except Exception as e:
|
|
return Err(
|
|
Report(
|
|
message=f"Error retrieving app status from Redis cache: {e}", error=e
|
|
)
|
|
)
|
|
|
|
|
|
async def set_cached_app_status_failed(
|
|
error: ErrorMessage,
|
|
redis: Redis | None = None,
|
|
) -> Result[None, Report]:
|
|
"""Stores the app status update failure in the Redis cache."""
|
|
logger.trace("set_cached_app_status_failed()")
|
|
try:
|
|
match await redis_set(
|
|
AppsServiceKeys.APP_STATUS_UPDATE_FAILED_KEY,
|
|
error.model_dump_json(),
|
|
ex=CACHE_TTL_SECONDS,
|
|
custom_redis=redis,
|
|
):
|
|
case Ok(_):
|
|
logger.debug(
|
|
f"App status update failure cache updated. "
|
|
f"Key: {AppsServiceKeys.APP_STATUS_UPDATE_FAILED_KEY}"
|
|
)
|
|
return Ok(None)
|
|
case Err(e):
|
|
logger.error(
|
|
"Error storing app status update failure in Redis cache."
|
|
f"Key: {AppsServiceKeys.APP_STATUS_UPDATE_FAILED_KEY}"
|
|
)
|
|
return Err(e)
|
|
except Exception as e:
|
|
return Err(
|
|
Report(
|
|
message=f"Error storing app status update failure in Redis cache: {e}",
|
|
error=e,
|
|
)
|
|
)
|
|
|
|
|
|
async def set_cached_app_status(
|
|
status: AppStatusQueryResult,
|
|
redis: Redis | None = None,
|
|
) -> Result[None, Report]:
|
|
"""Stores the app status in the Redis cache."""
|
|
logger.trace("set_cached_app_status()")
|
|
try:
|
|
json_data = status.model_dump_json()
|
|
match await redis_set(
|
|
AppsServiceKeys.APP_STATUS_CACHE_KEY,
|
|
json_data,
|
|
ex=CACHE_TTL_SECONDS,
|
|
custom_redis=redis,
|
|
):
|
|
case Ok(_):
|
|
logger.debug(
|
|
"App status cache updated. "
|
|
f"Key: {AppsServiceKeys.APP_STATUS_CACHE_KEY}"
|
|
)
|
|
case Err(e):
|
|
logger.error(
|
|
"Error storing app status in Redis cache."
|
|
f"Key: {AppsServiceKeys.APP_STATUS_CACHE_KEY}"
|
|
)
|
|
return Err(e)
|
|
|
|
match await redis_set(
|
|
AppsServiceKeys.APP_STATUS_TIMESTAMP_KEY,
|
|
status.timestamp,
|
|
custom_redis=redis,
|
|
ex=CACHE_TTL_SECONDS,
|
|
):
|
|
case Ok(_):
|
|
logger.debug(
|
|
"App status cache updated. "
|
|
f"Key: {AppsServiceKeys.APP_STATUS_TIMESTAMP_KEY}"
|
|
)
|
|
case Err(e):
|
|
logger.error(
|
|
"Error storing app status in Redis cache."
|
|
f"Key: {AppsServiceKeys.APP_STATUS_TIMESTAMP_KEY}"
|
|
)
|
|
return Err(e)
|
|
|
|
logger.debug(
|
|
f"App status cache updated. Key: {AppsServiceKeys.APP_STATUS_CACHE_KEY}"
|
|
)
|
|
|
|
return Ok(None)
|
|
except Exception as e:
|
|
return Err(Report(message="Error storing app status in Redis cache", error=e))
|
|
|
|
|
|
async def get_cache_timestamp(
|
|
redis: Redis | None = None,
|
|
) -> Result[Optional[int], Report]:
|
|
"""Retrieves the timestamp of the last cache update."""
|
|
logger.trace("get_cache_timestamp()")
|
|
try:
|
|
timestamp_str = await redis_get(
|
|
AppsServiceKeys.APP_STATUS_TIMESTAMP_KEY, custom_redis=redis
|
|
)
|
|
if timestamp_str is None or timestamp_str == "":
|
|
return Ok(None)
|
|
|
|
timestamp = int(timestamp_str)
|
|
if not isinstance(timestamp, int):
|
|
return Err(
|
|
Report(message="Unable to convert timestamp to an int from Redis")
|
|
)
|
|
|
|
return Ok(timestamp if timestamp else None)
|
|
except Exception as e:
|
|
return Err(
|
|
Report(message="Error retrieving cache timestamp from Redis", error=e)
|
|
)
|