From 380bf46fb61325027e4df157234ff5dba7064f82 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 6 Dec 2025 01:58:05 -0800 Subject: [PATCH] fix(coordinator): add row-level locking to prevent concurrent update conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Multiple parties reporting completion simultaneously caused lost updates because each transaction would read the full session, modify their participant status, then update ALL participants - causing last-write-wins behavior. Solution: Add SELECT ... FOR UPDATE locks on both mpc_sessions and participants tables at the start of the Update transaction. This serializes concurrent updates and prevents lost updates. Lock order: 1. Lock session row (FOR UPDATE) 2. Lock all participant rows for this session (FOR UPDATE) 3. Perform updates 4. Commit (releases locks) This ensures that concurrent ReportCompletion calls are fully serialized and each participant status update is preserved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../output/postgres/session_postgres_repo.go | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/mpc-system/services/session-coordinator/adapters/output/postgres/session_postgres_repo.go b/backend/mpc-system/services/session-coordinator/adapters/output/postgres/session_postgres_repo.go index 323a9089..26ec866e 100644 --- a/backend/mpc-system/services/session-coordinator/adapters/output/postgres/session_postgres_repo.go +++ b/backend/mpc-system/services/session-coordinator/adapters/output/postgres/session_postgres_repo.go @@ -249,6 +249,15 @@ func (r *SessionPostgresRepo) Update(ctx context.Context, session *entities.MPCS } defer tx.Rollback() + // Lock the session row first to prevent concurrent modifications + // This ensures serializable isolation for the entire session update + _, err = tx.ExecContext(ctx, ` + SELECT id FROM mpc_sessions WHERE id = $1 FOR UPDATE + `, session.ID.UUID()) + if err != nil { + return err + } + // Update session _, err = tx.ExecContext(ctx, ` UPDATE mpc_sessions SET @@ -265,8 +274,16 @@ func (r *SessionPostgresRepo) Update(ctx context.Context, session *entities.MPCS return err } - // Update each participant individually using UPDATE to avoid lost updates - // Using individual UPDATE statements ensures concurrent updates to different participants don't conflict + // Lock all participant rows for this session to prevent concurrent modifications + // This prevents lost updates when multiple parties report completion simultaneously + _, err = tx.ExecContext(ctx, ` + SELECT id FROM participants WHERE session_id = $1 FOR UPDATE + `, session.ID.UUID()) + if err != nil { + return err + } + + // Update each participant individually for _, p := range session.Participants { // Try UPDATE first result, err := tx.ExecContext(ctx, `