{"record":{"id":"d2048bf6ba4e56b1","repo":"666ghj/MiroFish","slug":"wait-timeout-0f-s","errorCode":null,"errorMessage":"模拟仍在停止中，图谱写入未在 {wait_timeout:.0f}s 内完成","messagePattern":"模拟仍在停止中，图谱写入未在 (.+?)s 内完成","errorType":"exception","errorClass":"SimulationStopPending","httpStatus":null,"severity":"warning","filePath":"backend/app/services/simulation_runner.py","lineNumber":1038,"sourceCode":"            not retrying_finalization\n            and\n            monitor is not None\n            and monitor is not threading.current_thread()\n            and monitor.is_alive()\n        ):\n            wait_timeout = max(\n                30.0,\n                ZEP_INGESTION_WAIT_TIMEOUT_SECONDS\n                + ZEP_HTTP_REQUEST_TIMEOUT_SECONDS\n                + 5,\n            )\n            monitor.join(timeout=wait_timeout)\n            if monitor.is_alive():\n                # The monitor still owns finalization and may be inside one\n                # bounded HTTP request. Do not block on or overwrite its lock;\n                # leave the observable state as STOPPING and let polling expose\n                # the eventual STOPPED/FAILED result.\n                raise SimulationStopPending(\n                    f\"模拟仍在停止中，图谱写入未在 {wait_timeout:.0f}s 内完成\"\n                )\n        else:\n            # Restart recovery or tests may have no monitor thread. Complete\n            # the same barrier synchronously in this request.\n            with cls._finalization_lock(simulation_id):\n                state = cls.get_run_state(simulation_id) or state\n                if cls._graph_memory_enabled.get(simulation_id, False):\n                    try:\n                        ZepGraphMemoryManager.stop_updater(simulation_id)\n                        cls._graph_memory_enabled.pop(simulation_id, None)\n                    except Exception as error:\n                        state.runner_status = RunnerStatus.FAILED\n                        state.twitter_running = False\n                        state.reddit_running = False\n                        state.completed_at = datetime.now().isoformat()\n                        state.error = f\"Zep图谱写入未完整完成: {error}\"\n                        cls._save_run_state(state)","sourceCodeStart":1020,"sourceCodeEnd":1056,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/services/simulation_runner.py#L1020-L1056","documentation":"SimulationStopPending (a TimeoutError subclass) is raised when stop_simulation has signaled termination but the background monitor thread still owns finalization after waiting wait_timeout = max(30, ZEP_INGESTION_WAIT_TIMEOUT + ZEP_HTTP_REQUEST_TIMEOUT + 5) seconds — typically because Zep graph-memory HTTP ingestion is slow. The runner deliberately does NOT contend for the finalization lock; observable state stays STOPPING and polling will eventually show STOPPED or FAILED.","triggerScenarios":"Stopping a graph-memory-enabled simulation while the Zep updater is still flushing activity batches to the Zep Cloud API, with each HTTP request bounded by ZEP_HTTP_REQUEST_TIMEOUT; total flush exceeds the computed wait_timeout.","commonSituations":"Slow or rate-limited Zep Cloud API at stop time; large activity backlog (long simulations) still being batched; network degradation between backend and Zep.","solutions":["Do not treat this as failure: catch SimulationStopPending and keep polling get_run_state/status until runner_status becomes STOPPED or FAILED.","If it persists, check Zep API health/latency and network; raise ZEP_INGESTION_WAIT_TIMEOUT_SECONDS / ZEP_HTTP_REQUEST_TIMEOUT_SECONDS if your graphs are large.","Avoid stopping mid-write when possible — let the simulation finish naturally so the flush happens outside a stop deadline."],"exampleFix":"// before\nstate = SimulationRunner.stop_simulation(sim_id);  # raises after ~35s of pending graph writes\n\n// after\nfrom app.services.simulation_runner import SimulationStopPending\ntry:\n    state = SimulationRunner.stop_simulation(sim_id)\nexcept SimulationStopPending:\n    state = SimulationRunner.wait_terminal_state(sim_id, poll=5.0)  # poll runner_status until STOPPED/FAILED","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from app.services.simulation_runner import SimulationStopPending\n\ntry:\n    state = SimulationRunner.stop_simulation(sim_id)\nexcept SimulationStopPending:\n    state = poll_until_terminal(sim_id)  # GET status every 5s; STOPPING is expected here","preventionTips":["Never assume stop() is synchronous when graph memory is enabled — always handle SimulationStopPending.","Size ZEP_INGESTION_WAIT_TIMEOUT_SECONDS to your expected activity backlog.","Prefer natural completion over manual stop for very long runs."],"tags":["simulation","stop","timeout","zep","graph-memory","async-finalization"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}