user1180596
user1180596

Reputation: 139

ActiveRecord find_or_initialize_by race conditions

I have a scenario where 2 db connections might both run Model.find_or_initialize_by(params) and raise an error: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint

I'd like to update my code so it could gracefully recover from it. Something like:

record = nil

begin
  record = Model.find_or_initialize_by(params)
rescue ActiveRecord::RecordNotUnique
  record = Model.where(params).first
end

return record

The trouble is that there's not a nice/easy way to reproduce this on my local machine, so I'm not confident that my fix actually works.

So I thought I'd get a bit creative and try calling create 2 times (locally) in a row which should raise then PG::UniqueViolation: ERROR, then I could rescue from it and make sure everything is handled gracefully.

But I get this error: PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block

I get this error even when I wrap everything in individual transaction blocks

record = nil

Model.transaction do
  record = Model.create(params)
end

begin
  Model.transaction do
    record = Model.create(params)
  end
rescue ActiveRecord::RecordNotUnique
end

Model.transaction do
  record = Model.where(params).first
end

return record

My questions:

I imagine there's probably something simple that I'm missing here, but it's late and perhaps I'm not thinking too clearly.

I'm running postgres 9.3 and rails 4.

EDIT Turns out that find_or_initialize_by should have been find_or_create_by and the errors I was getting was from the actual save call that happened later on in execution. #VeryTiredWhenIWroteThis

Upvotes: 3

Views: 3668

Answers (1)

dre-hh
dre-hh

Reputation: 8044

Has this actually happenend?

Model.find_or_initialize_by(params)

should never raise an ´ActiveRecord::RecordNotUnique´ error as it is not saving anything to db. It just creates a new ActiveRecord.

However in the second snippet you are creating records. create (without bang) does not throw exceptions caused by validations, but ActiveRecord::RecordNotUnique is always thrown in case of a duplicate by both create and create!

If you're creating records you don't need transactions at all. As Postgres being ACID compliant guarantees that only one of the both operations succeeds and if it responds so it's changes will be durable. (a single statement query against postgres is also a transaction). So your above code is almost fine if you replace through find_or_create_by

begin
 record = Model.find_or_create_by(params)
rescue ActiveRecord::RecordNotUnique
 record = Model.where(params).first
end

You can test if the code behaves correctly by simply trying to create the same record twice in row. However this will not test ActiveRecord::RecordNotUnique is actually thrown correctly on race conditions.

It's also no the responsibility of your app to test and testing it is not easy. You would have to start rails in multithread mode on your machine, or test against a multi process staging rails instance. Webrick for example handles only one request at a time. You can use puma application server, however on MRI there is no true concurrency (GIL). Threads only share the GIL only on IO blocking. Because talking to Postgres is IO, i'd expect some concurrent requests, but to be 100% sure, the best testing scenario would be to deploy on passenger with multiple workers and then use jmeter to run concurrent request agains the server.

Upvotes: 7

Related Questions