Add unique index to poll voters table
Note that Rails 7.1 changes `find_or_create_by!` so it calls `create_or_find_by!` when no record is found, meaning we'll rarely get `RecordNotUnique` exceptions when using this method during a race condition. Adding this index means we need to remove the uniqueness validation. According to the `create_or_find_by` documentation [1]: > Columns with unique database constraints should not have uniqueness > validations defined, otherwise create will fail due to validation > errors and find_by will never be called. We're adding a test that checks what happens when using `create_or_find_by!`. Note that we're creating voters combining `create_with` with `find_or_create_by!`. Using `find_or_create_by!(...)` with all attributes (including non-key ones like `origin`) fails when a voter already exists with different values, e.g. an existing `origin: "web"` and an incoming `"booth"`. In this situation the existing record is not matched and the unique index raises an exception. `create_with(...).find_or_create_by!(user: ..., poll: ...)` searches by the unique key only and applies the extra attributes only on creation. Existing voters are returned unchanged, which is the intended behavior. For the `take_votes_from` method, we're handling a (highly unlikely, but theoretically possible) scenario where a user votes at the same time as taking voters from another user. For that, we're doing something similar to what `create_or_find_by!` does: we're updating the `user_id` column inside a new transaction (using a new transactions avoids a `PG::InFailedSqlTransaction` exception when there are duplicate records), and deleting the existing voter when we get a `RecordNotUnique` exception. On `Poll::WebVote` we're simply raising an exception when there's already a user who's voted via booth, because the `Poll::WebVote#update` method should never be called in this case. We still need to use `with_lock` in `Poll::WebVote`, but not due to duplicate voters (`find_or_create_by!` method will now handle the unique record scenario, even in the case of simultaneous transactions), but because we use a uniqueness validation in `Poll::Answer`; this validation would cause an error in simultaneous transactions. [1] https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Relation.html#method-i-create_or_find_by
This commit is contained in:
@@ -4,49 +4,6 @@ describe "polls tasks" do
|
||||
let(:poll) { create(:poll) }
|
||||
let(:user) { create(:user, :level_two) }
|
||||
|
||||
describe "polls:remove_duplicate_voters" do
|
||||
before { Rake::Task["polls:remove_duplicate_voters"].reenable }
|
||||
|
||||
it "removes duplicate voters" do
|
||||
second_user = create(:user, :level_two)
|
||||
|
||||
voter = create(:poll_voter, poll: poll, user: user)
|
||||
second_voter = create(:poll_voter, poll: poll, user: second_user)
|
||||
other_user_voter = create(:poll_voter, poll: poll, user: create(:user, :level_two))
|
||||
other_poll_voter = create(:poll_voter, poll: create(:poll), user: user)
|
||||
|
||||
2.times { insert(:poll_voter, poll_id: poll.id, user_id: user.id) }
|
||||
insert(:poll_voter, poll_id: poll.id, user_id: second_user.id)
|
||||
|
||||
expect(Poll::Voter.count).to eq 7
|
||||
|
||||
Rake.application.invoke_task("polls:remove_duplicate_voters")
|
||||
|
||||
expect(Poll::Voter.count).to eq 4
|
||||
expect(Poll::Voter.all).to match_array [voter, second_voter, other_user_voter, other_poll_voter]
|
||||
end
|
||||
|
||||
it "removes duplicate voters on tenants" do
|
||||
create(:tenant, schema: "voters")
|
||||
|
||||
Tenant.switch("voters") do
|
||||
poll = create(:poll)
|
||||
user = create(:user, :level_two)
|
||||
|
||||
create(:poll_voter, poll: poll, user: user)
|
||||
insert(:poll_voter, poll_id: poll.id, user_id: user.id)
|
||||
|
||||
expect(Poll::Voter.count).to eq 2
|
||||
end
|
||||
|
||||
Rake.application.invoke_task("polls:remove_duplicate_voters")
|
||||
|
||||
Tenant.switch("voters") do
|
||||
expect(Poll::Voter.count).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "polls:remove_duplicate_answers" do
|
||||
before { Rake::Task["polls:remove_duplicate_answers"].reenable }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user