views:

39

answers:

2

I have a special case Model which must not become part of an outer transaction:

Outer.Transaction do
  ...

  Inner.create(:blah)

  ...
end

How do I stop Inner becoming part of the transaction, assuming Inner should know nothing about which specific transaction it is getting pulled into?

For example, creating an inner transaction is a no go, because that too will become part of the outer transaction.

I want to do this because the inner model needs to write immediately and not wait for the outer transaction to commit.

A: 

I'm curious as to what would require such a construct!

I think you're going to struggle to do it without a bit of hackery as you've described. For example, you could, in mysql, set the table storage type for Inner to one that doesn't support transactions (MyIsam eg) whilst keeping other classes' tables storage with something that does support transactions (YUK!).

If you can, you'd almost certainly be better off delaying the Inner.create until after the transaction. You can use begin with ensure to make sure that the create always happens. Something like:

create_inner = false
begin
  Outer.transaction.do
    ...
    create_inner = true # instead of Inner.create(:blah)
    ...
  end
ensure
  if create_inner
    Inner.create(:blah)
  end
end

This would become more complicated if the rest of your block depends on the created Inner instance. You could probably create the instance in the block and set created_inner to false at the end of the block so that, if the code runs without exception it will have been created in the transaction and you won't create again in the ensure.

If you want to do it in the general case you could define a class method on Inner to execute a block but always create an Inner object. You'd need to add an after_create to Inner too. You would rely on the Inner.create call in the block to create it when the transaction succeeds but if it is rolled back then you'd need to create it afterwards. For example:

class Inner < ActiveRecord::Base

  def self.ensure_created(&block)
    Thread.current[:created_inner] = false        
    begin
      block.call
    rescue => e
      if Thread.current[:created_inner]
        Inner.create(:blah)
      end
      raise e
    end
  end

  def after_create
    # Flag that an instance has been created in this thread so
    # that if we rollback out of a transaction we can create again
    Thread.current[:created_inner] = true
  end

You'd then call it like:

Inner.ensure_created do
  Outer.transaction do
    ...
    Inner.create(:blah)
    ...
  end
end

HOWEVER, there's plenty of downsides to this approach and I'm not sure I'd advocate it. It is complicated. It won't work if ActiveRecord::Rollback is raised as that exception won't bubble out of the Outer.transaction but will cause the Inner instance not to be created. It won't work properly when two or more calls are nested. And finally I haven't tested it thoroughly - use with caution!

Shadwell
Thanks for the suggestions! To answer he scenario question, its basically for locking batch job tasks, where "Inner" is a locking model. If 2 batches try to run at the time I want the 2nd batch to fail rather than wait in line, or there could be a pile up. The outer transaction was actually stopping this failure from occurring, causing each locking to wait in turn.
Greg Malcolm
I didn't end up following any of the suggestions offered, but I'm marking this as the Accepted Answer because it was the best attempt. Thanks guys!
Greg Malcolm
@greg malcolm: if you didn't follow any of the suggestions... what did you do?
marcgg
@marcgg: On analysis the transaction wasn't really needed, so it was removed. Not the perfect solution, but it was the lesser evil.
Greg Malcolm
A: 

You could define a seperate database connection for Inner then the transaction will only apply on Outer's connection.

Oliver