Brian Law
Brian Law

Reputation: 639

Multiple first or create in sidekiq creates multiple records instead of updating it

When a user is created, I am trying to add 1 to the count if a record exists, or create a record with count: 1 if not.

I have something like this:

class User < ApplicationRecord
  after_commit :record, on: :create

  def record
    SomeWorker.perform_async(id)
  end
end

After a user is created, SomeWorker will fire in sidekiq, and will call something like:

class SomeWorker
  include Sidekiq::Worker

  def perform(id)
    Record.where(some_conditions).first_or_create()
  end
end

It works like charm when I am not using sidekiq and just call it right after a user is created. However, when I seed data that creates 10 users, I expect to create 1 record with count: 10, but it creates 10 records with count: 1.

Any ideas what is happening and how I can fix it?

Upvotes: 1

Views: 852

Answers (1)

kiddorails
kiddorails

Reputation: 13014

It's the classic race condition. Please note that "Sidekiq is multithreaded so your Workers must be thread-safe.". All your workers are running in parallel and none of them initially finds a matching record in where and ends up creating the new record with count 1.

I will advice to instead create/find the record beforehand and send it's id for incrementing the count value. Though, I'm not sure why you are using Sidekiq to offload a basic UPDATE operation.

class User < ApplicationRecord
  after_commit :record, on: :create

  def record
    record = Record.create_with(count: 0).find_by(some_conditions)
    SomeWorker.perform_async(record.id)
  end
end

class SomeWorker
  include Sidekiq::Worker

  def perform(id)
    Record.find(id).increment!(:count)
  end
end

Also, please note that increment! is not suitable for concurrency either, but it should work in your case.

a = Record.first #<Record id: 1, count: 1>
b = Record.first #<Record id: 1, count: 1>
a.increment!(:count) #<Record id: 1, count: 2>
b.increment!(:count) #<Record id: 1, count: 2> ## SHOULD BE 3
b.reload #<Record id: 1, count: 3> It's actually three, but object was stale on update.

SQL fires the query below which bumps the value from the last set value.

UPDATE `records` SET `count` = COALESCE(`parent_id`, 0) + 1 WHERE `records`.`id` = 1

Upvotes: 1

Related Questions