666ghj/MiroFish · error · GraphInUseError

Graph is in use by active consumer(s)

Error message

Graph {graph_id} is in use by active consumer(s): {', '.join(active_simulations)}

What it means

GraphInUseError raised in _delete_cloud_graph_if_present (backend/app/api/graph.py) when a project's referenced Zep Cloud graph is about to be deleted but _active_graph_consumers still reports simulations using that graph_id. Deletion happens inside graph_lifecycle_lock(graph_id) so the consumer check and the Cloud DELETE are atomic per graph, preventing a race where a simulation starts querying a graph mid-deletion.

Solutions

  1. Stop or finish all running simulations for the project, then retry the graph delete.
  2. List active consumers from the error message (it names the active_simulations) and verify each one's real state.
  3. If a simulation is genuinely orphaned (worker died but DB row says active), mark it terminated/cleaned in the task manager so _active_graph_consumers stops counting it.
  4. For rebuild flows, wire the UI to stop simulations automatically before requesting deletion instead of surfacing this error to users.

Example fix

# before
@router.delete("/projects/{project_id}/graph")
def delete_graph(project_id: str):
    _delete_cloud_graph_if_present(project.graph_id)  # raises GraphInUseError to the client

# after - stop consumers first, then delete inside the same lifecycle domain
@router.delete("/projects/{project_id}/graph")
def delete_graph(project_id: str):
    active = _active_graph_consumers(project.graph_id)
    if active:
        for sim_id in active:
            simulation_service.stop(sim_id)  # or return 409 with the consumer list
    _delete_cloud_graph_if_present(project.graph_id)
Defensive patterns

Strategy: validation

Validate before calling

active = _active_graph_consumers(project.graph_id)
if active:
    # either stop them or refuse with a 409 payload naming them
    return {"detail": {"code": "graph_in_use", "consumers": active}}

Try / catch

try:
    _delete_cloud_graph_if_present(project.graph_id)
except GraphInUseError as e:
    raise HTTPException(status_code=409, detail=str(e)) from e

Prevention

When it happens

Trigger: Calling the graph delete/rebuild endpoint while any simulation for the project is still running (active simulations hold the graph_id); concurrent delete requests for the same graph (second one blocks on the re-entrant lock and re-checks consumers); a simulation stuck in an active state after a crashed worker so it never releases the graph.

Common situations: User hits 'delete graph' or 'rebuild' from the UI while Step5 simulations are open; orphaned/zombie simulation tasks after a backend restart keep the graph marked in-use; test suites that create simulations and delete graphs without tearing down simulations first.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/94d1b07c37619320. Report an issue: GitHub.

Appendix: source

Thrown at backend/app/api/graph.py:87

            continue
        run_state = SimulationRunner.get_run_state(simulation.simulation_id)
        if run_state and run_state.runner_status in active_runner_statuses:
            active.add(simulation.simulation_id)
    return sorted(active)


def _delete_cloud_graph_if_present(graph_id: str | None) -> None:
    """Delete a referenced Cloud graph without retrying the mutation."""

    if not graph_id:
        return
    # Keep the consumer check and Cloud mutation in one critical section. The
    # callers that also clear local references hold this re-entrant lock around
    # both operations.
    with graph_lifecycle_lock(graph_id):
        active_simulations = _active_graph_consumers(graph_id)
        if active_simulations:
            raise GraphInUseError(
                f"Graph {graph_id} is in use by active consumer(s): "
                f"{', '.join(active_simulations)}"
            )
        try:
            GraphBuilderService(api_key=Config.ZEP_API_KEY).delete_graph(graph_id)
        except NotFoundError:
            logger.info("Zep Cloud graph already absent: %s", graph_id)


def _clear_project_graph_reference(project) -> None:
    project.graph_id = None
    project.graph_build_task_id = None
    project.zep_batch_id = None
    project.zep_batch_operation_id = None
    project.error = None


def _project_build_lock(project_id: str) -> threading.Lock:

View on GitHub (pinned to b5b53acc57)