FIX: Upsert topic votes count atomically (#71)

FIX: Upsert topic votes count atomically (#71)

The Topic#update_vote_count method should be concurrency-safe to prevent unique constraint violation errors when multiple processes try to insert a topic_votes_count record for the same topic simultaneously.

Signed-off-by: OsamaSayegh asooomaasoooma90@gmail.com

diff --git a/app/models/discourse_voting/category_setting.rb b/app/models/discourse_voting/category_setting.rb
index 75dba4a..2b234b7 100644
--- a/app/models/discourse_voting/category_setting.rb
+++ b/app/models/discourse_voting/category_setting.rb
@@ -38,3 +38,17 @@ module DiscourseVoting
     end
   end
 end
+
+# == Schema Information
+#
+# Table name: discourse_voting_category_settings
+#
+#  id          :bigint           not null, primary key
+#  category_id :integer
+#  created_at  :datetime         not null
+#  updated_at  :datetime         not null
+#
+# Indexes
+#
+#  index_discourse_voting_category_settings_on_category_id  (category_id) UNIQUE
+#
diff --git a/app/models/discourse_voting/topic_vote_count.rb b/app/models/discourse_voting/topic_vote_count.rb
index 14a0830..b2fb3a6 100644
--- a/app/models/discourse_voting/topic_vote_count.rb
+++ b/app/models/discourse_voting/topic_vote_count.rb
@@ -7,3 +7,18 @@ module DiscourseVoting
     belongs_to :topic
   end
 end
+
+# == Schema Information
+#
+# Table name: discourse_voting_topic_vote_count
+#
+#  id          :bigint           not null, primary key
+#  topic_id    :integer
+#  votes_count :integer
+#  created_at  :datetime         not null
+#  updated_at  :datetime         not null
+#
+# Indexes
+#
+#  index_discourse_voting_topic_vote_count_on_topic_id  (topic_id) UNIQUE
+#
diff --git a/app/models/discourse_voting/vote.rb b/app/models/discourse_voting/vote.rb
index e148825..88cb90d 100644
--- a/app/models/discourse_voting/vote.rb
+++ b/app/models/discourse_voting/vote.rb
@@ -8,3 +8,19 @@ module DiscourseVoting
     belongs_to :topic
   end
 end
+
+# == Schema Information
+#
+# Table name: discourse_voting_votes
+#
+#  id         :bigint           not null, primary key
+#  topic_id   :integer
+#  user_id    :integer
+#  archive    :boolean          default(FALSE)
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+# Indexes
+#
+#  index_discourse_voting_votes_on_user_id_and_topic_id  (user_id,topic_id) UNIQUE
+#
diff --git a/lib/discourse_voting/topic_extension.rb b/lib/discourse_voting/topic_extension.rb
index 55f2d6a..befed9e 100644
--- a/lib/discourse_voting/topic_extension.rb
+++ b/lib/discourse_voting/topic_extension.rb
@@ -27,8 +27,16 @@ module DiscourseVoting
     def update_vote_count
       count = self.votes.count
 
-      topic_vote_count = self.topic_vote_count || DiscourseVoting::TopicVoteCount.new(topic: self)
-      topic_vote_count.update!(votes_count: count)
+      DB.exec(<<~SQL, topic_id: self.id, votes_count: count)
+        INSERT INTO discourse_voting_topic_vote_count
+        (topic_id, votes_count, created_at, updated_at)
+        VALUES
+        (:topic_id, :votes_count, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+        ON CONFLICT (topic_id) DO UPDATE SET
+          votes_count = :votes_count,
+          updated_at = CURRENT_TIMESTAMP
+          WHERE discourse_voting_topic_vote_count.topic_id = :topic_id
+      SQL
     end
 
     def who_voted
diff --git a/spec/lib/topic_extension_spec.rb b/spec/lib/topic_extension_spec.rb
new file mode 100644
index 0000000..0b8af53
--- /dev/null
+++ b/spec/lib/topic_extension_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe DiscourseVoting::TopicExtension do
+  let(:user) { Fabricate(:user) }
+  let(:user2) { Fabricate(:user) }
+
+  let(:topic) { Fabricate(:topic) }
+  let(:topic2) { Fabricate(:topic) }
+
+  before do
+    SiteSetting.voting_enabled = true
+    SiteSetting.voting_show_who_voted = true
+  end
+
+  describe '#update_vote_count' do
+    it 'upserts topic votes count' do
+      topic.update_vote_count
+      topic2.update_vote_count
+
+      expect(topic.reload.topic_vote_count.votes_count).to eq(0)
+      expect(topic2.reload.topic_vote_count.votes_count).to eq(0)
+
+      DiscourseVoting::Vote.create!(user: user, topic: topic)
+      topic.update_vote_count
+      topic2.update_vote_count
+
+      expect(topic.reload.topic_vote_count.votes_count).to eq(1)
+      expect(topic2.reload.topic_vote_count.votes_count).to eq(0)
+
+      DiscourseVoting::Vote.create!(user: user2, topic: topic)
+      DiscourseVoting::Vote.create!(user: user, topic: topic2)
+      topic.update_vote_count
+      topic2.update_vote_count
+
+      expect(topic.reload.topic_vote_count.votes_count).to eq(2)
+      expect(topic2.reload.topic_vote_count.votes_count).to eq(1)
+    end
+  end
+end

GitHub sha: 8dbc75f9

This commit appears in #71 which was approved by ZogStriP. It was merged by OsamaSayegh.