From 6686011f914d006e4b62a8cddc614f7f7f34e12c Mon Sep 17 00:00:00 2001 From: FeelRatchanons Date: Sat, 13 Jun 2026 10:22:11 +0700 Subject: [PATCH] fix(simulation): make create_simulation idempotent to prevent duplicate sims Creating a simulation was not idempotent: each POST /api/simulation/create minted a fresh sim_. The frontend wraps createSimulation in requestWithRetry(..., 3, 1000), so a slow/timed-out first request triggers a retry that creates a second identical simulation for the same project+graph (observed: two sims created ~2s apart). Guard against this by reusing an existing, not-yet-run simulation for the same project_id + graph_id (and matching platform flags) when status is CREATED/PREPARING/READY and current_round == 0. A force_new=True escape hatch preserves the ability to intentionally create a fresh simulation. Verified: repeated /create calls now return the same simulation_id instead of spawning duplicates. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/simulation_manager.py | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py index 0d161a90..0b3f0a2d 100644 --- a/backend/app/services/simulation_manager.py +++ b/backend/app/services/simulation_manager.py @@ -197,19 +197,43 @@ class SimulationManager: graph_id: str, enable_twitter: bool = True, enable_reddit: bool = True, + force_new: bool = False, ) -> SimulationState: """ 创建新的模拟 - + Args: project_id: 项目ID graph_id: Zep图谱ID enable_twitter: 是否启用Twitter模拟 enable_reddit: 是否启用Reddit模拟 - + force_new: 强制新建,跳过幂等复用(默认False) + Returns: SimulationState """ + # 幂等防重复:若已存在同一 project+graph 且尚未运行的模拟,则直接复用, + # 避免前端重试(requestWithRetry)或重复提交导致创建出多个相同的模拟。 + if not force_new: + reusable_statuses = ( + SimulationStatus.CREATED, + SimulationStatus.PREPARING, + SimulationStatus.READY, + ) + for existing in self.list_simulations(project_id=project_id): + if ( + existing.graph_id == graph_id + and existing.enable_twitter == enable_twitter + and existing.enable_reddit == enable_reddit + and existing.current_round == 0 + and existing.status in reusable_statuses + ): + logger.info( + f"复用未运行的模拟(幂等防重复): {existing.simulation_id}, " + f"project={project_id}, graph={graph_id}, status={existing.status.value}" + ) + return existing + import uuid simulation_id = f"sim_{uuid.uuid4().hex[:12]}"