{"record":{"id":"ea12daad5413a9c8","repo":"666ghj/MiroFish","slug":"simulation-id-ea12da","errorCode":null,"errorMessage":"模拟已在运行或结束处理中: {simulation_id}","messagePattern":"模拟已在运行或结束处理中: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/app/services/simulation_runner.py","lineNumber":437,"sourceCode":"            total_simulation_hours=total_hours,\n            started_at=datetime.now().isoformat(),\n        )\n        \n        # Atomically claim this simulation ID. The expensive updater/process\n        # startup happens after releasing the lock, while the persisted\n        # STARTING state makes every concurrent start fail closed.\n        with cls._finalization_lock(simulation_id):\n            existing = cls.get_run_state(simulation_id)\n            active_statuses = {\n                RunnerStatus.STARTING,\n                RunnerStatus.RUNNING,\n                RunnerStatus.PAUSED,\n                RunnerStatus.STOPPING,\n            }\n            if (\n                existing and existing.runner_status in active_statuses\n            ) or ZepGraphMemoryManager.get_updater(simulation_id) is not None:\n                raise ValueError(f\"模拟已在运行或结束处理中: {simulation_id}\")\n            cls._save_run_state(state)\n        \n        # 如果启用图谱记忆更新，创建更新器\n        if enable_graph_memory_update:\n            if not graph_id:\n                raise ValueError(\"启用图谱记忆更新时必须提供 graph_id\")\n            \n            try:\n                ZepGraphMemoryManager.create_updater(simulation_id, graph_id)\n                cls._graph_memory_enabled[simulation_id] = True\n                logger.info(f\"已启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}\")\n            except Exception as e:\n                logger.error(f\"创建图谱记忆更新器失败: {e}\")\n                cls._graph_memory_enabled[simulation_id] = False\n                state.runner_status = RunnerStatus.FAILED\n                state.error = f\"Zep图谱更新器初始化失败: {e}\"\n                with cls._finalization_lock(simulation_id):\n                    cls._save_run_state(state)","sourceCodeStart":419,"sourceCodeEnd":455,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/services/simulation_runner.py#L419-L455","documentation":"Raised in SimulationRunner's start path under a per-simulation finalization lock: if existing run state has runner_status in {STARTING, RUNNING, PAUSED, STOPPING}, or ZepGraphMemoryManager still has an updater registered for the simulation, it raises ValueError('模拟已在运行或结束处理中'). This is a deliberate fail-closed concurrency guard against double-start and against racing the shutdown/finalization path.","triggerScenarios":"Calling start twice (double-click, frontend retry, duplicate webhook); starting while a previous run is PAUSED or mid-STOPPING; a stale run state left RUNNING after a crash or backend kill -9; an orphaned ZepGraphMemoryManager updater left behind by a failed prior stop.","commonSituations":"Backend crashed without finalizing state, leaving status RUNNING on disk; user resumes from a paused run by calling start instead of resume; two API replicas both accepting a start request; STOPPING stuck because the subprocess is hung.","solutions":["Stop the existing run first (stop endpoint) and wait for runner_status to leave the active set before starting again.","If the status is stale after a crash, stop/cleanup the run state (or reset runner_status to a terminal value via the recovery path) then start.","For a PAUSED run, use the resume flow rather than start.","If stuck in STOPPING, check and kill the simulation subprocess, then let finalization complete."],"exampleFix":"# before\nSimulationRunner.start_simulation(sim_id, ...)\n# after\nstate = SimulationRunner.get_run_state(sim_id)\nif state and state.runner_status in {'STARTING', 'RUNNING', 'PAUSED', 'STOPPING'}:\n    SimulationRunner.stop_simulation(sim_id)\n    # wait for terminal status / cleanup, then start\nSimulationRunner.start_simulation(sim_id, ...)","handlingStrategy":"validation","validationCode":"active = {'STARTING', 'RUNNING', 'PAUSED', 'STOPPING'}\nstate = SimulationRunner.get_run_state(sim_id)\nif state and state.runner_status in active:\n    SimulationRunner.stop_simulation(sim_id)  # or resume if PAUSED\n    wait_for_terminal_status(sim_id)","typeGuard":null,"tryCatchPattern":"try:\n    SimulationRunner.start_simulation(sim_id, ...)\nexcept ValueError as e:\n    if '已在运行' in str(e):\n        raise HTTPException(409, str(e)) from e  # conflict: stop or resume first\n    raise","preventionTips":["Map this ValueError to HTTP 409 so clients can distinguish double-start from bad input.","Provide crash recovery: reset stale RUNNING state on backend startup if no subprocess exists.","Use resume for PAUSED runs; reserve start for terminal states."],"tags":["simulation","concurrency","state-machine","double-start","locking"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}