Files
evo-sync/web/health_checker.py

135 lines
4.7 KiB
Python
Raw 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)