Serving Audit Report — SoccerPredictAI
Date: 2026-04-24
Auditor: GitHub Copilot (Claude Sonnet 4.6)
Scope: FastAPI endpoints, model loading, Celery async, batch serving, error handling
Method: анализ src/app/routers/, src/app/services/predict.py, src/app/tasks/predict.py, src/app/schemas/predict.py, src/app/worker_ml.py
1. API Endpoints Audit
1.1 Инвентаризация endpoints
Endpoint
Method
Назначение
Sync/Async
Auth
GET /predict/matches/
GET
Список upcoming матчей из batch inference
Sync
❌ (нет)
POST /predict/
POST
Предсказание по inline features
Sync (Celery 30s timeout)
❌
GET /predict/{match_id}
GET
Предсказание по ID (lookup)
Sync (Celery 30s timeout)
❌
POST /predict/async/
POST
Async предсказание → task_id
Async
❌
GET /predict/model/info
GET
MLflow model metadata
Sync (Celery)
❌
GET /monitoring/task_status/{task_id}
GET
Статус Celery task
Sync
❌
GET /monitoring/celery/queues
GET
Статистика очередей
Sync
❌
GET /monitoring/celery/workers
GET
Статистика workers
Sync
❌
GET /livescores/
GET
Livescores из PostgreSQL
Sync
❌
PATCH /sources/livescores/
PATCH
Trigger scraping
Async (Celery)
✅ X-Token
GET /sources/export/{table}
GET
Export table → MinIO
Async (Celery)
✅ X-Token
GET /healthcheck/
GET
Liveness check
Sync
❌
GET /metrics
GET
Prometheus scrape
Sync
❌
⚠️ P1 Security: /predict/* endpoints не требуют аутентификации — любой может запрашивать предсказания. Это может быть намеренным (публичный сервис), но не задокументировано как решение.
1.2 Request / Response schemas
Endpoint
Request schema
Response schema
Validated
Расхождения
POST /predict/
PredictRequest
PredictResponse
✅ Pydantic
—
GET /predict/{match_id}
path param int
PredictResponse
✅
—
POST /predict/async/
AsyncPredictRequest (match_id: int)
AsyncPredictResponse
✅
—
GET /predict/model/info
—
ModelInfoResponse
✅
—
GET /predict/matches/
—
list[dict]
⚠️ нет Pydantic schema
⚠️ P2: GET /predict/matches/ возвращает list[dict] без Pydantic response model — нет schema validation ответа.
1.3 Error responses
Сценарий
HTTP код
Реализован
match_id не найден
404
✅
ML worker timeout (30s)
504
✅
ML inference error
500
✅
Неверный request body
422 (Pydantic)
✅
MLflow недоступен
500 (через retry)
✅
Celery недоступен
—
⚠️ вернёт 500, нет явного 503
2. Model Loading Audit
2.1 Механизм загрузки
# worker_process_init signal → PredictionService.load() → mlflow.pyfunc.load_model()
model_uri = f "models:/ { self . _model_name } @ { self . _model_stage } "
# = "models:/soccer_clf@champion"
Аспект
Статус
Источник модели
MLflow Registry по alias champion ✅
Hardcoded путь
❌ нет
Lazy loading с double-checked locking
✅ thread-safe
Cold-start avoidance (worker_process_init)
✅
Fallback при cold init outside worker
✅ lazy fallback с warning
2.2 Проблема reload
⚠️ P1: Модель загружается один раз при старте worker process. При обновлении champion alias в MLflow Registry — worker не перезагружает модель. Требуется ручной restart Celery workers.
2.3 pyfunc predict_proba fallback
raw = model . predict ( df )
if hasattr ( raw , "ndim" ) and raw . ndim == 2 :
return raw # Already (N, 3) probabilities
# Fallback: 1-D label output → try sklearn predict_proba
✅ Надёжный fallback для разных MLflow flavours.
3. Batch Inference Serving
Batch artifacts path
DVC batch_inference → data/predictions/match_features.parquet
→ (optional) upload to MinIO predictions/match_features.parquet
↓
FeatureLookupService._load()
├── local file если существует (dev)
└── MinIO (production K8s)
Caching strategy
# MinIO re-check interval: FEATURE_CACHE_CHECK_INTERVAL = 60s (default)
# Cache invalidation: MinIO LastModified изменился → перезагрузить
Аспект
Статус
Local file cache
✅ по mtime
MinIO stale detection
✅ по LastModified с 60s polling
Graceful degraded mode
✅ stale cache + warning при недоступности MinIO
Thread-safety
⚠️ нет lock при MinIO reload — concurrent requests могут вызвать double-load
Redis prediction cache
Аспект
Статус
Cache key
predict:{match_id}:{run_id}
TTL
PREDICTION_CACHE_TTL env (default 3600s)
Redis unavailable
graceful degradation — caching disabled
Cache hit метрика
cached: true в ответе
4. Celery Task Audit
predict_match task
Аспект
Значение
Queue
ml
max_retries
2
default_retry_delay
10s
Idempotency
✅ (same input → same output; Redis cache)
State updates
✅ PROGRESS state
task_time_limit
3600s
task_acks_late
✅ — ack только после успешного выполнения
task_reject_on_worker_lost
✅ — задача переназначается при потере worker
⚠️ P2: max_retries=2 при default_retry_delay=10s — total retry window = 20s. При 30s sync timeout в FastAPI: retry закончатся до timeout, но клиент уже получил 504. Последний retry может всё равно выполниться.
Async flow (POST /predict/async/)
POST /predict/async/
↓
predict_match_task.apply_async(queue="ml")
↓ returns task_id
AsyncPredictResponse(task_id=...)
↓
GET /monitoring/task_status/{task_id}
↓
{"status": "SUCCESS"|"FAILURE"|"PROGRESS", "result": {...}}
✅ Полный async flow реализован. Polling endpoint существует.
5. Findings
ID
Severity
Описание
SRV-01
P1
Нет auth на prediction endpoints (/predict/*) — открытый доступ
SRV-02
P1
Нет механизма автоматического reload модели при смене champion alias
SRV-03
P2
GET /predict/matches/ возвращает list[dict] без Pydantic response schema — нет гарантий формата
SRV-04
P2
FeatureLookupService._load() при MinIO reload нет threading.Lock — concurrent requests могут вызвать параллельную загрузку
SRV-05
P2
predict_match retry (2×10s) + FastAPI sync timeout (30s) — race condition: последний retry может выполниться после 504 возвращён клиенту
SRV-06
P2
Нет явного 503 при недоступности Celery broker — клиент получит 500 или timeout
SRV-07
P3
asyncio.get_event_loop() устарел в Python 3.10+, deprecated в 3.12 — нужен asyncio.get_running_loop()