{"record":{"id":"5c6c812d3f1800b3","repo":"666ghj/MiroFish","slug":"simulation-id-status-state-runner-stat","errorCode":null,"errorMessage":"模拟未在运行: {simulation_id}, status={state.runner_status}","messagePattern":"模拟未在运行: (.+?), status=(.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"backend/app/services/simulation_runner.py","lineNumber":991,"sourceCode":"\n            pending_updater = ZepGraphMemoryManager.get_updater(simulation_id)\n            retrying_finalization = (\n                pending_updater is not None\n                and state.runner_status in {\n                    RunnerStatus.STOPPING,\n                    RunnerStatus.FAILED,\n                }\n            )\n            if (\n                state.runner_status not in [\n                    RunnerStatus.STARTING,\n                    RunnerStatus.RUNNING,\n                    RunnerStatus.PAUSED,\n                    RunnerStatus.STOPPING,\n                ]\n                and not retrying_finalization\n            ):\n                raise ValueError(\n                    f\"模拟未在运行: {simulation_id}, status={state.runner_status}\"\n                )\n\n            state.runner_status = RunnerStatus.STOPPING\n            cls._manual_stop_requests.add(simulation_id)\n            cls._save_run_state(state)\n            cls._sync_simulation_status(simulation_id, RunnerStatus.STOPPING)\n\n            # 终止进程\n            process = cls._processes.get(simulation_id)\n            if process and process.poll() is None:\n                try:\n                    cls._terminate_process(process, simulation_id)\n                except ProcessLookupError:\n                    pass\n                except Exception as e:\n                    logger.error(f\"终止进程组失败: {simulation_id}, error={e}\")\n                    try:","sourceCodeStart":973,"sourceCodeEnd":1009,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/services/simulation_runner.py#L973-L1009","documentation":"stop_simulation refuses to stop a run whose runner_status is a terminal/inactive state (anything other than STARTING, RUNNING, PAUSED, STOPPING) and that is not eligible for finalization retry (no pending Zep updater). This is a lifecycle guard against double-stop and against stopping runs that never really started.","triggerScenarios":"Calling stop on a run already STOPPED or FAILED (and retrying_finalization is False because there is no pending ZepGraphMemoryManager updater); racing with the monitor thread that just finalized the run between your get_run_state check and the stop call.","commonSituations":"Frontend 'Stop' button not disabled when the status poll shows a terminal state; two concurrent stop requests; retry of a timed-out stop request after the run already finalized.","solutions":["Refresh runner_status via get_run_state / the status endpoint immediately before calling stop, and skip stop for STOPPED/FAILED/terminal states.","Make the client idempotent: catch this ValueError and re-read status; if terminal, treat the stop as satisfied.","Guard the Stop button in the UI against states other than STARTING/RUNNING/PAUSED."],"exampleFix":"// before\ntry:\n    SimulationRunner.stop_simulation(sim_id)\nexcept ValueError as e:\n    raise  # surfaces 模拟未在运行 to the user\n\n// after\nACTIVE = {RunnerStatus.STARTING, RunnerStatus.RUNNING, RunnerStatus.PAUSED, RunnerStatus.STOPPING}\nstate = SimulationRunner.get_run_state(sim_id)\nif state and state.runner_status not in ACTIVE:\n    return state  # already terminal, nothing to stop\nSimulationRunner.stop_simulation(sim_id);","handlingStrategy":"validation","validationCode":"ACTIVE_STOP_STATES = {\n    RunnerStatus.STARTING, RunnerStatus.RUNNING,\n    RunnerStatus.PAUSED, RunnerStatus.STOPPING,\n}\n\ndef should_request_stop(sim_id: str) -> bool:\n    s = SimulationRunner.get_run_state(sim_id)\n    return s is not None and s.runner_status in ACTIVE_STOP_STATES","typeGuard":null,"tryCatchPattern":"try:\n    SimulationRunner.stop_simulation(sim_id)\nexcept ValueError as e:\n    if \"模拟未在运行\" in str(e):\n        return SimulationRunner.get_run_state(sim_id)  # already terminal\n    raise","preventionTips":["Disable the Stop UI action for terminal statuses.","Re-check status immediately before issuing stop to shrink the race window.","Return 409 Conflict from the API layer for this error so clients can react."],"tags":["simulation","stop","state-machine","idempotency"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}