feat(v2): stream execution revisions over sse
This commit is contained in:
@@ -2,7 +2,7 @@ import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncGenerator, AsyncIterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, cast
|
||||
@@ -32,6 +32,7 @@ from backup_tool.db.models import (
|
||||
User,
|
||||
)
|
||||
from backup_tool.execution import (
|
||||
TERMINAL_STATES,
|
||||
EnqueueError,
|
||||
enqueue,
|
||||
public_event,
|
||||
@@ -57,8 +58,30 @@ from backup_tool.security.redaction import redact
|
||||
from backup_tool.security.secrets import EnvelopeCipher
|
||||
|
||||
|
||||
async def single_execution_event(event: dict[str, object]) -> AsyncIterator[str]:
|
||||
yield f"event: execution\\ndata: {json.dumps(event)}\\n\\n"
|
||||
def execution_event_id(execution: Execution) -> str:
|
||||
"""Stable monotonic cursor for an execution's current durable revision."""
|
||||
updated = execution.updated_at.astimezone(UTC).isoformat()
|
||||
return f"{updated}:{execution.state}:{execution.attempt}"
|
||||
|
||||
|
||||
async def execution_events_stream(
|
||||
sessions: async_sessionmaker[AsyncSession], execution_id: str, last_event_id: str | None
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Poll durable execution state and yield each changed redacted revision as SSE."""
|
||||
last_sent = last_event_id
|
||||
while True:
|
||||
async with sessions() as stream_db:
|
||||
execution = await stream_db.get(Execution, execution_id)
|
||||
if execution is None:
|
||||
return
|
||||
event_id = execution_event_id(execution)
|
||||
if event_id != last_sent:
|
||||
payload = public_event(execution)
|
||||
yield (f"id: {event_id}\\nevent: execution\\ndata: {json.dumps(payload)}\\n\\n")
|
||||
last_sent = event_id
|
||||
if execution.state in TERMINAL_STATES:
|
||||
return
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
class Problem(Exception):
|
||||
@@ -838,14 +861,15 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
execution_id: str,
|
||||
db: Annotated[AsyncSession, Depends(session)],
|
||||
identity: Annotated[tuple[User, set[str], bool], Depends(actor)],
|
||||
last_event_id: Annotated[str | None, Header(alias="Last-Event-ID")] = None,
|
||||
) -> StreamingResponse:
|
||||
enforce_scope(identity[1], "execution:read")
|
||||
execution = await db.get(Execution, execution_id)
|
||||
if execution is None:
|
||||
if await db.get(Execution, execution_id) is None:
|
||||
raise Problem(404, "resource_not_found", "Execution was not found.")
|
||||
|
||||
return StreamingResponse(
|
||||
single_execution_event(public_event(execution)), media_type="text/event-stream"
|
||||
execution_events_stream(app.state.sessions, execution_id, last_event_id),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
@app.get("/api/v2/audit")
|
||||
|
||||
Reference in New Issue
Block a user