fix(coordinator): prevent lost updates in concurrent participant status changes
Fix critical concurrency bug where simultaneous ReportCompletion calls from multiple parties could cause lost database updates. Changed from UPSERT-all to UPDATE-individual pattern to ensure each participant status update is atomic and won't be overwritten by concurrent transactions. Before: All participants were UPSERTed in single transaction, causing last-commit-wins behavior that lost earlier status updates. After: Each participant is UPDATEd individually using UPDATE...WHERE, then INSERT only if row doesn't exist. This prevents concurrent updates to different participants from conflicting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
00b48bab50
commit
aab88834f9
|
|
@ -265,34 +265,66 @@ func (r *SessionPostgresRepo) Update(ctx context.Context, session *entities.MPCS
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert participants (insert or update)
|
// Update each participant individually using UPDATE to avoid lost updates
|
||||||
|
// Using individual UPDATE statements ensures concurrent updates to different participants don't conflict
|
||||||
for _, p := range session.Participants {
|
for _, p := range session.Participants {
|
||||||
_, err = tx.ExecContext(ctx, `
|
// Try UPDATE first
|
||||||
INSERT INTO participants (
|
result, err := tx.ExecContext(ctx, `
|
||||||
id, session_id, party_id, party_index, status,
|
UPDATE participants SET
|
||||||
device_type, device_id, platform, app_version, public_key, joined_at, completed_at
|
status = $1,
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
public_key = $2,
|
||||||
ON CONFLICT (session_id, party_id) DO UPDATE SET
|
completed_at = $3,
|
||||||
status = EXCLUDED.status,
|
device_type = $4,
|
||||||
public_key = EXCLUDED.public_key,
|
device_id = $5,
|
||||||
completed_at = EXCLUDED.completed_at
|
platform = $6,
|
||||||
|
app_version = $7
|
||||||
|
WHERE session_id = $8 AND party_id = $9
|
||||||
`,
|
`,
|
||||||
uuid.New(),
|
|
||||||
session.ID.UUID(),
|
|
||||||
p.PartyID.String(),
|
|
||||||
p.PartyIndex,
|
|
||||||
p.Status.String(),
|
p.Status.String(),
|
||||||
|
p.PublicKey,
|
||||||
|
p.CompletedAt,
|
||||||
p.DeviceInfo.DeviceType,
|
p.DeviceInfo.DeviceType,
|
||||||
p.DeviceInfo.DeviceID,
|
p.DeviceInfo.DeviceID,
|
||||||
p.DeviceInfo.Platform,
|
p.DeviceInfo.Platform,
|
||||||
p.DeviceInfo.AppVersion,
|
p.DeviceInfo.AppVersion,
|
||||||
p.PublicKey,
|
session.ID.UUID(),
|
||||||
p.JoinedAt,
|
p.PartyID.String(),
|
||||||
p.CompletedAt,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no rows affected, participant doesn't exist yet, INSERT it
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
// Participant doesn't exist, INSERT it
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO participants (
|
||||||
|
id, session_id, party_id, party_index, status,
|
||||||
|
device_type, device_id, platform, app_version, public_key, joined_at, completed_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
`,
|
||||||
|
uuid.New(),
|
||||||
|
session.ID.UUID(),
|
||||||
|
p.PartyID.String(),
|
||||||
|
p.PartyIndex,
|
||||||
|
p.Status.String(),
|
||||||
|
p.DeviceInfo.DeviceType,
|
||||||
|
p.DeviceInfo.DeviceID,
|
||||||
|
p.DeviceInfo.Platform,
|
||||||
|
p.DeviceInfo.AppVersion,
|
||||||
|
p.PublicKey,
|
||||||
|
p.JoinedAt,
|
||||||
|
p.CompletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue