{"record":{"id":"94d1b07c37619320","repo":"666ghj/MiroFish","slug":"graph-graph-id-is-in-use-by-active-consumer-s","errorCode":null,"errorMessage":"Graph {graph_id} is in use by active consumer(s): {', '.join(active_simulations)}","messagePattern":"Graph (.+?) is in use by active consumer\\(s\\): (.+?)","errorType":"exception","errorClass":"GraphInUseError","httpStatus":409,"severity":"error","filePath":"backend/app/api/graph.py","lineNumber":87,"sourceCode":"            continue\n        run_state = SimulationRunner.get_run_state(simulation.simulation_id)\n        if run_state and run_state.runner_status in active_runner_statuses:\n            active.add(simulation.simulation_id)\n    return sorted(active)\n\n\ndef _delete_cloud_graph_if_present(graph_id: str | None) -> None:\n    \"\"\"Delete a referenced Cloud graph without retrying the mutation.\"\"\"\n\n    if not graph_id:\n        return\n    # Keep the consumer check and Cloud mutation in one critical section. The\n    # callers that also clear local references hold this re-entrant lock around\n    # both operations.\n    with graph_lifecycle_lock(graph_id):\n        active_simulations = _active_graph_consumers(graph_id)\n        if active_simulations:\n            raise GraphInUseError(\n                f\"Graph {graph_id} is in use by active consumer(s): \"\n                f\"{', '.join(active_simulations)}\"\n            )\n        try:\n            GraphBuilderService(api_key=Config.ZEP_API_KEY).delete_graph(graph_id)\n        except NotFoundError:\n            logger.info(\"Zep Cloud graph already absent: %s\", graph_id)\n\n\ndef _clear_project_graph_reference(project) -> None:\n    project.graph_id = None\n    project.graph_build_task_id = None\n    project.zep_batch_id = None\n    project.zep_batch_operation_id = None\n    project.error = None\n\n\ndef _project_build_lock(project_id: str) -> threading.Lock:","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/api/graph.py#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Stop or finish all running simulations for the project, then retry the graph delete.","List active consumers from the error message (it names the active_simulations) and verify each one's real state.","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.","For rebuild flows, wire the UI to stop simulations automatically before requesting deletion instead of surfacing this error to users."],"exampleFix":"# before\n@router.delete(\"/projects/{project_id}/graph\")\ndef delete_graph(project_id: str):\n    _delete_cloud_graph_if_present(project.graph_id)  # raises GraphInUseError to the client\n\n# after - stop consumers first, then delete inside the same lifecycle domain\n@router.delete(\"/projects/{project_id}/graph\")\ndef delete_graph(project_id: str):\n    active = _active_graph_consumers(project.graph_id)\n    if active:\n        for sim_id in active:\n            simulation_service.stop(sim_id)  # or return 409 with the consumer list\n    _delete_cloud_graph_if_present(project.graph_id)","handlingStrategy":"validation","validationCode":"active = _active_graph_consumers(project.graph_id)\nif active:\n    # either stop them or refuse with a 409 payload naming them\n    return {\"detail\": {\"code\": \"graph_in_use\", \"consumers\": active}}","typeGuard":null,"tryCatchPattern":"try:\n    _delete_cloud_graph_if_present(project.graph_id)\nexcept GraphInUseError as e:\n    raise HTTPException(status_code=409, detail=str(e)) from e","preventionTips":["Always stop simulations before issuing graph delete/rebuild; make the UI orchestrate stop-then-delete.","Tear down simulations in test fixtures before deleting graphs.","Reconcile zombie 'active' simulations after worker crashes so they stop blocking deletes.","Return 409 with the consumer list so clients can present actionable choices."],"tags":["backend","python","zep","lifecycle","concurrency","conflict"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}