Brian
Brian

Reputation: 7145

Run rails code after an update to the database has commited, without after_commit

I'm trying to battle some race cases with my background task manager. Essentially, I have a Thing object (already exists) and assign it some properties, and then save it. After it is saved with the new properties, I queue it in Resque, passing in the ID.

thing = Thing.find(1)
puts thing.foo # outputs "old value"
thing.foo = "new value"
thing.save
ThingProcessor.queue_job(thing.id)

The background job will load the object from the database using Thing.find(thing_id).

The problem is that we've found Resque is so fast at picking up the job and loading the Thing object from the ID, that it loads a stale object. So within the job, calling thing.foo will still return "old value" like 1/100 times (not real data, but it does not happen often).

We know this is a race case, because rails will return from thing.save before the data has actually been commit to the database (postgresql in this case).

Is there a way in Rails to only execute code AFTER a database action has commit? Essentially I want to make sure that by the time Resque loads the object, it is getting the freshest object. I know this can be achieved using an after_commit hook on the Thing model, but I don't want it there. I only need this to happen in this one specific context, not every time the model has commit changed to the DB.

Upvotes: 13

Views: 4308

Answers (4)

akostadinov
akostadinov

Reputation: 18614

One gem to allow that is https://github.com/Ragnarson/after_commit_queue

It is a little different than the other answer's after_commit_everywhere gem. The after_commit_everywhere call seems decoupled from current model being saved or not.

So it might be what you expect or not expect, depending on your use case.

Upvotes: 0

Paul Danelli
Paul Danelli

Reputation: 1114

I had a similar issue, where by I needed to ensure that a transaction had commited before running a series of action. I ended up using this Gem:

https://github.com/Envek/after_commit_everywhere

It meant that I could do the following:

def finalize!
  Order.transaction do
    payment.charge!

    # ...

    # Ensure that we only send out items and perform after actions when the order has defintely be completed
    after_commit { OrderAfterFinalizerJob.perform_later(self) }
  end
end

Upvotes: 0

Edilson Borges
Edilson Borges

Reputation: 587

You can put in a transaction as well. Just like the example below:

transaction do
  thing = Thing.find(1)
  puts thing.foo # outputs "old value"
  thing.foo = "new value"
  thing.save
end
ThingProcessor.queue_job(thing.id)

Update: there is a gem which calls After Transaction, with this you may solve your problem. Here is the link: http://xtargets.com/2012/03/08/understanding-and-solving-race-conditions-with-ruby-rails-and-background-workers/

Upvotes: 4

JCQ
JCQ

Reputation: 623

What about wrapping a try around the transaction so that the job is queued up only upon success of the transaction?

Upvotes: 0

Related Questions