feat(host-protocol): long poll assignments

This commit is contained in:
2026-07-12 18:12:33 +08:00
parent efb754fbe7
commit c7f8d0c128
3 changed files with 121 additions and 2 deletions
@@ -1,5 +1,8 @@
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from time import monotonic
from typing import TYPE_CHECKING
from fastapi import APIRouter, HTTPException, Request, status
@@ -8,7 +11,13 @@ from cloud.auth import (
AuthProvider,
HostAuthorizationError,
)
from cloud.internal_api.models import HeartbeatRequest, HeartbeatResponse
from cloud.internal_api.models import (
AssignmentModel,
ClaimRequest,
ClaimResponse,
HeartbeatRequest,
HeartbeatResponse,
)
from core.models import Device, utc_now
if TYPE_CHECKING:
@@ -20,7 +29,11 @@ def create_internal_router(
pool: DevicePool,
auth_provider: AuthProvider,
version_prefix: str = "/internal/v1",
claim_poll_interval_seconds: float = 0.1,
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
) -> APIRouter:
if claim_poll_interval_seconds <= 0:
raise ValueError("claim_poll_interval_seconds must be greater than zero")
router = APIRouter(prefix=version_prefix, tags=["host-agent"])
def authorize_host(request: Request, host_id: str) -> None:
@@ -66,6 +79,44 @@ def create_internal_router(
received_at=utc_now(),
)
@router.post(
"/hosts/{host_id}/assignments/claim",
response_model=ClaimResponse,
)
async def claim_assignment(
host_id: str,
payload: ClaimRequest,
request: Request,
) -> ClaimResponse:
authorize_host(request, host_id)
if payload.host_id != host_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="claim host_id must match the request path",
)
deadline = monotonic() + payload.timeout_seconds
while True:
assignment = pool.store.claim_assignment(host_id=host_id, now=utc_now())
if assignment is not None:
return ClaimResponse(
assignment=AssignmentModel(
task_id=assignment.task_id,
attempt=assignment.attempt,
lease_id=assignment.lease_id,
lease_expires_at=assignment.lease_expires_at,
host_id=assignment.host_id,
device_id=assignment.device_id,
goal=assignment.goal,
workflow_definition_id=assignment.workflow_definition_id,
)
)
remaining = deadline - monotonic()
if remaining <= 0:
return ClaimResponse(timed_out=True)
await sleep(min(claim_poll_interval_seconds, remaining))
return router