From ded6aed15e74e5a12aa28971b7bd2e2ba87283cd Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Mon, 6 Feb 2023 20:32:45 +0400 Subject: [PATCH] FIX: avoid race condition when setting user status (#19817) (#20182) This is a backport of 84e13e9. We caught it in logs, race condition led to this error: ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "user_statuses_pkey" DETAIL: Key (user_id)=(15) already exists.) The reason the problem happened was that we were checking if a user has status and if not inserting status: if user_status ... else self.user_status = UserStatus.create!(status) end The problem is that it's possible that another request will insert status just after we check if status exists and just before our request call `UserStatus.create!(status)`. Using `upsert` fixes the problem because under the hood `upsert` generates the only SQL request that uses "INSERT ... ON CONFLICT DO UPDATE". So we do everything in one SQL query, and that query takes care of resolving possible conflicts. --- app/models/user.rb | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index d0d9846a433..51dd41348b5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1789,14 +1789,17 @@ class User < ActiveRecord::Base end def set_status!(description, emoji, ends_at = nil) - status = { description: description, emoji: emoji, set_at: Time.zone.now, ends_at: ends_at } - - if user_status - user_status.update!(status) - else - self.user_status = UserStatus.create!(status) - end + status = { + description: description, + emoji: emoji, + set_at: Time.zone.now, + ends_at: ends_at, + user_id: id, + } + validate_status!(status) + UserStatus.upsert(status) + reload_user_status publish_user_status(user_status) end @@ -2174,6 +2177,10 @@ class User < ActiveRecord::Base ) SQL end + + def validate_status!(status) + UserStatus.new(status).validate! + end end # == Schema Information