Files
evo-sync/web/health_checker.py

135 lines
4.7 KiB
Python
Raw Permalink Normal View History

import asyncio
import logging
from datetime import datetime, timedelta
import httpx
from web.database import SessionLocal
from web.models import EvotorConnection, VkConnection
logger = logging.getLogger("uvicorn.error")
EVOTOR_STORES_URL = "https://api.evotor.ru/stores"
EVOTOR_TOKEN_URL = "https://oauth.evotor.ru/oauth/token"
VK_USERS_GET_URL = "https://api.vk.com/method/users.get"
VK_API_VERSION = "5.131"
# Refresh Evotor token if it expires within this window
REFRESH_BEFORE_EXPIRY = timedelta(hours=2)
async def _refresh_evotor_token(conn: EvotorConnection) -> str | None:
"""Attempt to refresh the Evotor access token. Returns new access token or None."""
from web.config import settings
if not conn.refresh_token:
return None
try:
async with httpx.AsyncClient() as client:
resp = await client.post(
EVOTOR_TOKEN_URL,
data={
"grant_type": "refresh_token",
"refresh_token": conn.refresh_token,
},
auth=(settings.EVOTOR_CLIENT_ID, settings.EVOTOR_CLIENT_SECRET),
timeout=15,
)
if resp.status_code != 200:
return None
data = resp.json()
return data if data.get("access_token") else None
except Exception:
return None
async def check_evotor_connection(access_token: str) -> bool:
try:
async with httpx.AsyncClient() as client:
response = await client.get(
EVOTOR_STORES_URL,
headers={"Authorization": f"Bearer {access_token}"},
timeout=15,
)
return response.status_code == 200
except Exception:
return False
async def check_vk_connection(access_token: str) -> bool:
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
VK_USERS_GET_URL,
params={"access_token": access_token, "v": VK_API_VERSION},
timeout=10,
)
if resp.status_code != 200:
return False
data = resp.json()
return "error" not in data
except Exception:
return False
async def run_health_checks() -> None:
db = SessionLocal()
try:
now = datetime.utcnow()
evotor_connections = db.query(EvotorConnection).all()
for conn in evotor_connections:
# Proactively refresh if token expires soon
needs_refresh = (
conn.refresh_token and
conn.token_expires_at and
conn.token_expires_at - now < REFRESH_BEFORE_EXPIRY
)
if needs_refresh:
token_data = await _refresh_evotor_token(conn)
if token_data:
conn.access_token = token_data["access_token"]
conn.refresh_token = token_data.get("refresh_token", conn.refresh_token)
expires_in = token_data.get("expires_in")
conn.token_expires_at = now + timedelta(seconds=expires_in) if expires_in else None
logger.info("Refreshed Evotor token for user_id=%d", conn.user_id)
is_online = await check_evotor_connection(conn.access_token)
# If offline and not yet tried refresh, attempt it now
if not is_online and conn.refresh_token and not needs_refresh:
token_data = await _refresh_evotor_token(conn)
if token_data:
conn.access_token = token_data["access_token"]
conn.refresh_token = token_data.get("refresh_token", conn.refresh_token)
expires_in = token_data.get("expires_in")
conn.token_expires_at = now + timedelta(seconds=expires_in) if expires_in else None
is_online = await check_evotor_connection(conn.access_token)
if is_online:
logger.info("Evotor token refreshed after failed check for user_id=%d", conn.user_id)
conn.is_online = is_online
conn.last_checked_at = now
vk_connections = db.query(VkConnection).all()
for conn in vk_connections:
conn.is_online = await check_vk_connection(conn.access_token)
conn.last_checked_at = now
db.commit()
logger.info(
"Health checks completed: %d Evotor, %d VK",
len(evotor_connections),
len(vk_connections),
)
except Exception:
logger.exception("Error during health checks")
db.rollback()
finally:
db.close()
async def health_check_loop(interval: int) -> None:
while True:
await run_health_checks()
await asyncio.sleep(interval)