feat(mutator,web): live topology mutation pipeline backend (DEBT-030)
Wire the mutator and web API into the service bus so live-topology
edits flow sub-second from enqueue to UI:
- Mutator publishes every state transition on the bus (mutation.applying
/applied/failed + topology.status). Fire-and-forget; DB stays source
of truth.
- Mutator watch loop subscribes to topology.*.mutation.enqueued and
wakes early via asyncio.Event — the 10s poll becomes a fallback
heartbeat, not the primary dispatch trigger.
- POST /topologies/{id}/mutations publishes mutation.enqueued after
the DB write succeeds.
- New GET /topologies/{id}/events SSE route: snapshot on connect
(status + in-flight mutations), live forwards topology.{id}.>
bus events, 15s keepalive. ?token= auth mirrors /stream.
- New decnet/bus/app.py — process-wide lazy bus singleton for the
API, closed cleanly on lifespan shutdown.
This commit is contained in:
@@ -13,6 +13,9 @@ from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from decnet.bus import topics as _topics
|
||||
from decnet.bus.app import get_app_bus
|
||||
from decnet.logging import get_logger
|
||||
from decnet.telemetry import traced as _traced
|
||||
from decnet.topology.status import (
|
||||
TopologyStatus,
|
||||
@@ -27,6 +30,8 @@ from decnet.web.dependencies import repo, require_admin, require_viewer
|
||||
|
||||
from ._guards import get_topology_or_404, map_repo_exception
|
||||
|
||||
_log = get_logger("api.topology.mutations")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_MUTATABLE: frozenset[str] = frozenset(
|
||||
@@ -80,6 +85,20 @@ async def api_enqueue_mutation(
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
# Fire-and-forget bus publish so the mutator can wake immediately and
|
||||
# the SSE route can notify connected editors. Bus failure here must
|
||||
# never mask a successful enqueue — the DB row is authoritative.
|
||||
bus = await get_app_bus()
|
||||
if bus is not None:
|
||||
try:
|
||||
await bus.publish(
|
||||
_topics.topology_mutation(topology_id, _topics.MUTATION_ENQUEUED),
|
||||
{"mutation_id": mutation_id, "op": body.op, "payload": body.payload},
|
||||
event_type=_topics.MUTATION_ENQUEUED,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_log.warning("bus publish (enqueued) failed: %s", exc)
|
||||
|
||||
return MutationEnqueueResponse(mutation_id=mutation_id, state="pending")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user