feat: add foreign key constraints and metrics for blockchain node
This commit is contained in:
116
apps/pool-hub/src/poolhub/app/routers/match.py
Normal file
116
apps/pool-hub/src/poolhub/app/routers/match.py
Normal file
@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from redis.asyncio import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..deps import db_session_dep, redis_dep
|
||||
from ..prometheus import (
|
||||
match_candidates_returned,
|
||||
match_failures_total,
|
||||
match_latency_seconds,
|
||||
match_requests_total,
|
||||
)
|
||||
from poolhub.repositories.match_repository import MatchRepository
|
||||
from poolhub.repositories.miner_repository import MinerRepository
|
||||
from ..schemas import MatchCandidate, MatchRequestPayload, MatchResponse
|
||||
|
||||
router = APIRouter(tags=["match"])
|
||||
|
||||
|
||||
def _normalize_requirements(requirements: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return requirements or {}
|
||||
|
||||
|
||||
def _candidate_from_payload(payload: Dict[str, Any]) -> MatchCandidate:
|
||||
return MatchCandidate(**payload)
|
||||
|
||||
|
||||
@router.post("/match", response_model=MatchResponse, summary="Find top miners for a job")
|
||||
async def match_endpoint(
|
||||
payload: MatchRequestPayload,
|
||||
session: AsyncSession = Depends(db_session_dep),
|
||||
redis: Redis = Depends(redis_dep),
|
||||
) -> MatchResponse:
|
||||
start = time.perf_counter()
|
||||
match_requests_total.inc()
|
||||
|
||||
miner_repo = MinerRepository(session, redis)
|
||||
match_repo = MatchRepository(session, redis)
|
||||
|
||||
requirements = _normalize_requirements(payload.requirements)
|
||||
top_k = payload.top_k
|
||||
|
||||
try:
|
||||
request = await match_repo.create_request(
|
||||
job_id=payload.job_id,
|
||||
requirements=requirements,
|
||||
hints=payload.hints,
|
||||
top_k=top_k,
|
||||
)
|
||||
|
||||
active_miners = await miner_repo.list_active_miners()
|
||||
candidates = _select_candidates(requirements, payload.hints, active_miners, top_k)
|
||||
|
||||
await match_repo.add_results(
|
||||
request_id=request.id,
|
||||
candidates=candidates,
|
||||
)
|
||||
|
||||
match_candidates_returned.inc(len(candidates))
|
||||
duration = time.perf_counter() - start
|
||||
match_latency_seconds.observe(duration)
|
||||
|
||||
return MatchResponse(
|
||||
job_id=payload.job_id,
|
||||
candidates=[_candidate_from_payload(candidate) for candidate in candidates],
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - safeguards unexpected failures
|
||||
match_failures_total.inc()
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="match_failed") from exc
|
||||
|
||||
|
||||
def _select_candidates(
|
||||
requirements: Dict[str, Any],
|
||||
hints: Dict[str, Any],
|
||||
active_miners: List[tuple],
|
||||
top_k: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
min_vram = float(requirements.get("min_vram_gb", 0))
|
||||
min_ram = float(requirements.get("min_ram_gb", 0))
|
||||
capabilities_required = set(requirements.get("capabilities_any", []))
|
||||
region_hint = hints.get("region")
|
||||
|
||||
ranked: List[Dict[str, Any]] = []
|
||||
for miner, status, score in active_miners:
|
||||
if miner.gpu_vram_gb and miner.gpu_vram_gb < min_vram:
|
||||
continue
|
||||
if miner.ram_gb and miner.ram_gb < min_ram:
|
||||
continue
|
||||
if capabilities_required and not capabilities_required.issubset(set(miner.capabilities or [])):
|
||||
continue
|
||||
if region_hint and miner.region and miner.region != region_hint:
|
||||
continue
|
||||
|
||||
candidate = {
|
||||
"miner_id": miner.miner_id,
|
||||
"addr": miner.addr,
|
||||
"proto": miner.proto,
|
||||
"score": float(score),
|
||||
"explain": _compose_explain(score, miner, status),
|
||||
"eta_ms": status.avg_latency_ms if status else None,
|
||||
"price": miner.base_price,
|
||||
}
|
||||
ranked.append(candidate)
|
||||
|
||||
ranked.sort(key=lambda item: item["score"], reverse=True)
|
||||
return ranked[:top_k]
|
||||
|
||||
|
||||
def _compose_explain(score: float, miner, status) -> str:
|
||||
load = status.queue_len if status else 0
|
||||
latency = status.avg_latency_ms if status else "n/a"
|
||||
return f"score={score:.3f} load={load} latency={latency}"
|
||||
Reference in New Issue
Block a user