Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #5457 (reopened enhancement)

Opened 3 years ago

Last modified 1 year ago

ActiveRecord should use savepoints for nested transactions

Reported by: laas.toom@eenet.ee Assigned to: David
Priority: normal Milestone: 2.x
Component: ActiveRecord Version: 1.1.1
Severity: normal Keywords: savepoint nested transaction
Cc: bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer, lastobelus, brian.wong

Description

ActiveRecord disables nested transactions by using a mutex lock. But sometimes nested transactions are needed. For example when testing code that requires a transaction to be rolled back on some error (e.g. not enough money) and transactional fixtures are turned on.

For such cases I wrote a plugin for ActiveRecord to make usage of savepoints for enabling nested transactions functionality. Currently PostgreSQL and MySQL are implemented, though all databases that have some kind of savepoint mechanism could be supported.

This plugin can be reached at: http://rubyforge.org/projects/arnesttransacts

Is this something to be considered as a patch to core ActiveRecord? I could try to make this patch, but won't bother if you think this should remain a plugin.

Summary of patch:

  • rewrite ActiveRecord::ConnectionAdapters::DatabaseStatements.transaction to support possible savepoints
  • add create_savepoint, rollback_to_savepoint and release_savepoint methods to ActiveRecord::ConnectionAdapters::DatabaseStatements
  • implement those methods in actual connection adapters
  • if create_savepoint returns nil, then savepoints are considered not supported and nested transactions are not used.

Best regards, Laas Toom

Attachments

nested_transactions.rb (1.5 kB) - added by airbaggins on 12/19/06 17:04:30.
Alternative method.
nested_transactions_with_implicit_transaction_config.patch (14.6 kB) - added by tarmo on 07/02/07 19:57:39.
nested_transactions_with_implicit_transaction_config.2.patch (15.0 kB) - added by tarmo on 08/25/07 10:23:03.
Added Mysql support (in addition to Postgresql)
nested_transactions_with_implicit_transaction_config.3.patch (16.5 kB) - added by tarmo on 12/04/07 21:56:56.
Restored the support for nested transactions in tests and fixed some assert_queries() which with the nested transactions support are getting two more queries for the transaction creation. The broken delete tests indeed appear two now have two more queries, unrelated to the nested transactions.

Change History

06/21/06 19:30:28 changed by bitsweat

  • cc set to bitsweat.
  • keywords changed from activerecord savepoint to savepoint nested transaction.
  • version set to 1.1.1.
  • severity changed from trivial to normal.
  • priority changed from low to normal.

Hey Laas, nice work and good idea. I did some work with savepoints but found they are not too useful in the general case because Foo.transaction { ... } typically means 'require execution within a transaction', not 'require a new transaction'.

How about a module method to declare the transactional requirements of its methods:

class Person < ActiveRecord::Base
  transactional :requires => [:save, :destroy],
    :unsupported => :method_affecting_nontransactional_resource
end
class OrderTest < Test::Unit::TestCase
  transactional :require_new => :run, :nested => /^test_/
end

06/22/06 07:22:33 changed by laas.toom@eenet.ee

Hello!

Nice to hear that my ideas are welcome!

I think that in general case (as in testing with transactional fixtures) the code that uses a transaction does not have to know about enclosing transactions, so when it rolls back a transaction, it expects it to roll back to the nearest Foo.transaction{} declaration, therefore it should mean 'require new transaction'.

When it is interpreted as 'require execution in transaction' the following code will have unexpected results:

foo.name = "before"
foo.save
Foo.transaction(foo){
  foo.name="after1"
  foo.save
  begin
    Foo.transaction(foo){
      foo.name="after2"
      foo.save
      raise
    }
  rescue
    puts foo.name
  end
}

In current AR implementation, this would output "after2", but expected result would have been 'after1', because the inner transaction should have been rolled back, not just ignored.

I think that when knowingly nesting transactions, people expect them to roll back to the nearest declaration and when unaware of the nesting (e.g. in testing correct handling of rollback), they would be very surprised about the result (as above).

Your idea of requirement declaration is not bad, but I think it would mean unnecessary extra work for both AR developers to implement this behavior and to Rails users to declare all uses of nested/not-nested transactions etc.

IMHO making AR aware of savepoints does not affect the majority of rails users who do not need/use nested transactions as the savepoints are never created. But it does help those, who decide to use nested transactions and would spend days on debugging the results before digging into AR code to find out that inner transactions are just ignored (as I did).

As having savepoint when not knowing about them does not do anything bad, I vote for turning savepoints on by default.

PS. For the next month I will have very infrequent e-mail access, so I might not answer questions very promptly and do not have time to do proper dev work on this matter. Back on-line again in august.

Best, Laas

08/03/06 13:38:19 changed by jonathan.viney@gmail.com

  • cc changed from bitsweat to bitsweat, jonathan.viney@gmail.com.

+1

I've resorted to turning off transactional fixtures because of this problem.

12/19/06 17:04:30 changed by airbaggins

  • attachment nested_transactions.rb added.

Alternative method.

12/19/06 17:17:32 changed by airbaggins

  • cc changed from bitsweat, jonathan.viney@gmail.com to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net.

Just thought I´d add my two cents, having had to deal with this problem recently too. Sorry it´s not nicely packaged as a plugin, but you can drop it in your lib directory and require it in config/environment.rb. It is only a PostgreSQL one... but it should work for others that support saveponts, by just changing the name of the class and fixing the SQL for your database, if necessary.

How this one works:

  • Replaces the rollback_db_transaction, commit_db_transaction and begin_db_transaction. As all transactions (in the core, you might need to egrep for any '\<(BEGIN|START|TRANSACTION)\>' s in your vendor plugins) call these three functions only for, it will work for transactions anywhere.
  • Maintains a transaction nest count and starts a virtual nested transaction, using a series of savepoints if a new transaction is started from within an existing one.

It does require that calls to these functions are symmetric, ie. that each begin call is terminated by exactly one commit or rollback (which they are in the core rails).

Basically, it differs from the above approach in that it slightly 'lower level'.

I find it useful for avoiding all the problems with unit tests and fixtures and calling destroy and save from other transactions (as those two functions are implicitly wrapped). It will stop these committing in unit tests and producing all those warnings about transactions already started.

04/09/07 00:05:15 changed by gordon

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com.

This appears to be a derelict ticket, but hopefully I can revive some interest/discussion. I just ran into this problem when making some tests to check some transaction code. The tests insisted things were broken but manually performing those actions caused no problems.

While I agree that needing nested transactions in the actual application probably isn't that common, it seems as though anyone who writes any transaction code in their application (nested or not) that doesn't return an exception outside the outermost block will run into problems when they try to test it.

For now I've turned off transaction fixtures, but I really would like them on. Is there any hope for this change? The savepoint idea above seemed fairly promising, but it looks like the plugin hasn't been touched in almost a year so I don't know how viable it is at this point. If not, maybe include something in the docs indicating this could be a problem?

04/25/07 15:53:17 changed by tarmo

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo.

I've been using the transactional migrations plugin for a while now with great success, all my tests use transactional migrations. Would be great if this was integrated to rails core.

05/29/07 20:27:07 changed by josh

  • status changed from new to closed.
  • resolution set to wontfix.

transactions are deprecated

05/29/07 20:44:27 changed by tarmo

  • status changed from closed to reopened.
  • resolution deleted.

"Object transactions" are deprecated, but as far as I know normal database transactions are not deprecated so I don't think this ticket should be closed.

05/29/07 20:48:57 changed by bitsweat

  • status changed from reopened to closed.
  • resolution set to wontfix.
  • milestone set to 1.x.

The patch leads to a proliferation of savepoints when flat transactions are almost always what's needed.

Best implemented as a plugin with explicit savepoints.

05/30/07 02:42:59 changed by gordon

  • status changed from closed to reopened.
  • resolution deleted.

I really think you should reconsider this patch. The arguments against it seem to be

  1. No one ever needs it.
  2. It will significatly increase overhead

I don't think the first is true. Suppose someone had code like the following in their controller

begin
  myfoo = Foo.new
  mybaz = Baz.new
  Bar.transaction do
    mybar = Bar.new
    mybar.prop = "invalid value"
    mybar.save!
    myfoo.prop = "foo!"
    mybaz.prop = "baz!"
  end
  myfoo!
  mybaz!
rescue Exception => boom
  logger.error("Crikey! Invalid save attempt! Better look into that...")
ensure
  #some stuff
end

It would work when actually used in the interface, but any tests depending on the rollback of that transaction would fail because transactional fixtures causes invisible transaction nesting and the exception is caught in the controller. Actually let me get that on a line by itself.

In the above case, there is no bug but the tests fail.

That seems bad to me, and figuring out why the tests fail in this instance is incredibly difficult.

As for the overhead, I'm not an expert but it seems that if most people only need flat transactions then performance in the common case would not be greatly affected. Overall, it seems like this a low cost way to make a commonly misunderstood part of Rails behave as expected and remove some bad side effects along the way. If this particular patch is not good enough, I'd be happy to work something up.

05/30/07 03:15:13 changed by bitsweat

  • status changed from reopened to closed.
  • resolution set to wontfix.

It just doesn't come up much. Almost never. I appreciate your opinion here, but let's see it in plugin form garnering wide adoption first.

06/02/07 21:34:41 changed by snoopbaron

It just happened to me in one of my test cases. I believe this might come up more frequently than you assume.

06/03/07 00:30:56 changed by bitsweat

You can still test that a rollback occurred. I encourage you to write the plugin and see how it fares!

06/03/07 00:36:24 changed by tarmo

The plugin is almost a year old now, still works fine though: http://rubyforge.org/projects/arnesttransacts/

06/03/07 01:13:07 changed by bitsweat

Cool, that's a good starting point.

The plugin overuses savepoints: nested transaction transaction blocks are typically equivalent to EJB 'requires' declaration, but this plugin starts new pseudo-transactions for each, the equivalent of the far less common 'requires new' declaration.

06/03/07 01:40:36 changed by tarmo

If I understood you correctly, the problem is that for every "transaction { }" block a new (savepoint) transaction is created instead of the current one being reused (if one already exists) unless a new transaction is somehow explicitly required?

If so, then how do you think this requirement should be specified? Should implicit transactions inside save() and destroy() automatically require a new transaction?, should it somehow be optional? or should they always execute within their parent transaction if one exists and create one if not?

I'm also a bit concerned about the amount of savepoints being created, but I don't have a clear idea on how to best solve it.

06/03/07 01:52:58 changed by bitsweat

Yes, exactly. It breaks the existing transaction { ... } semantics.

For the plugin, I'd pass an argument to transaction or use a different method to explicitly indicate that you want to start a new transaction.

For example,

transaction { transaction { whatever } }

gives SQL

BEGIN
# whatever
COMMIT

whereas

transaction do
  transaction :requires_new do
    whatever
  end
end

gives SQL

BEGIN
SAVEPOINT marker
# whatever
RELEASE SAVEPOINT marker
COMMIT

I don't know a good API to express this, though. Ideas?

Perhaps

# Requires transaction, but does not start if one's already active.
transaction do
  # Requires transaction. Always start a new one.
  transaction :force => true do
    whatever
  end
end

06/03/07 02:05:41 changed by tarmo

There is also another issue, the implicit transactions, should it be possible to make an activerecord class default to running create/update/save/destroy inside an explicit transaction or should users who need explicit transactions always wrap those method calls inside a transaction block that forces a transaction?

If the former, then I'd suggest something like this:

class MyRecord < ActiveRecord::Base
  requires_new_transaction
  # or
  self.requires_new_transaction = true
end

Or perhaps even more flexible:

class MyRecord < ActiveRecord::Base
  requires_new_transaction :only => [ :create, :destroy ]
end

07/02/07 19:57:39 changed by tarmo

  • attachment nested_transactions_with_implicit_transaction_config.patch added.

07/02/07 20:13:18 changed by tarmo

I've attached a patch (against edge but should work with 1.2.3 with minor changes) which implements nested transactions similiar to the original arnesttransacts plugin, but also uses the savepoint naming convention from the "Alternative method" implementation posted in this ticket.

In addition to nested transactions there is also support for configuring "implicit" transaction type for activerecord classes. By default every activerecord class should behave as it does with the current rails transaction implementation (does not create another transaction if it already runs inside a transaction) but it is also possible to make either save (save, save! and update_attributes) or destroy methods to create a nested transaction even if they already run inside a transaction (Or not create a transaction at all. I took the liberty of adding this as another option, not sure if it's a good idea though.)

I also changed transactional tests support a bit so a transaction that is created inside a transactional test does not "see" the "outer" transaction, so an action (that uses transactions) that is run inside a transactional test should behave exactly the same as the action run inside a non-transactional test.

The patch only implements nested transactions for postgresql as it is the only database I'm familiar enough with, if anyone is interested in mysql (or some other db) support it would be great if you could try to add it in the same way it is already done for postgresql. (btw. I'm afraid a couple of tests now give different results depending on if the database has nested transactions support, this is because now the code inside the test does not see the outside transaction and thus the tested SQL query count changes (because of the savepoint creation), for these tests transactions should probably be switched off)

I'm considering also releasing this patch as a plug-in but at the moment I don't have a good place for hosting it yet.

Could anyone from the core team comment on the chances of something like this getting added to rails?

07/02/07 20:16:49 changed by tarmo

Oh, another thing. Currently my patch only allows configuring transaction type per-activerecord-class, perhaps it would be a good idea to allow global default configuration so one could say that all implicit transactions have to be nested by-default or that there should be no implicit transactions at all?

07/22/07 19:05:26 changed by tarmo

  • status changed from closed to reopened.
  • resolution deleted.

Reopening this so someone would notice it again.

08/25/07 10:23:03 changed by tarmo

  • attachment nested_transactions_with_implicit_transaction_config.2.patch added.

Added Mysql support (in addition to Postgresql)

09/05/07 17:37:35 changed by tolsen

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen.

09/27/07 15:07:35 changed by lifofifo

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo.

11/06/07 15:29:50 changed by jkraemer

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer.

12/04/07 19:41:05 changed by lastobelus

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer, lastobelus.

We are also experiencing the bug that no one would ever experience unless they used transactions and tested them.

Thanks for the patch tarmo. I applied it against 8142 and got the following test failures:

[[[

1) Failure:

test_transaction_type_destroy_flat(NestedTransactionsTest)

[./test/abstract_unit.rb:41:in `assert_queries'

./test/transactions_test.rb:370:in `test_transaction_type_destroy_flat' ./test/transactions_test.rb:390:in `with_topic_transaction_options' ./test/transactions_test.rb:369:in `test_transaction_type_destroy_flat' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `send' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `run']:

12 instead of 10 queries were executed. <10> expected but was <12>.

2) Failure:

test_transaction_type_destroy_nested(NestedTransactionsTest)

[./test/abstract_unit.rb:41:in `assert_queries'

./test/transactions_test.rb:376:in `test_transaction_type_destroy_nested' ./test/transactions_test.rb:390:in `with_topic_transaction_options' ./test/transactions_test.rb:375:in `test_transaction_type_destroy_nested' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `send' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `run']:

14 instead of 12 queries were executed. <12> expected but was <14>.

3) Failure:

test_transaction_type_destroy_none(NestedTransactionsTest)

[./test/abstract_unit.rb:41:in `assert_queries'

./test/transactions_test.rb:364:in `test_transaction_type_destroy_none' ./test/transactions_test.rb:390:in `with_topic_transaction_options' ./test/transactions_test.rb:363:in `test_transaction_type_destroy_none' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `send' /opt/local/lib/ruby/gems/1.8/gems/mocha-0.5.5/lib/mocha/test_case_adapter.rb:19:in `run']:

10 instead of 8 queries were executed. <8> expected but was <10>. ]]]

Have you tested your patch against a version of rails that recent? Unfortunately mysql transactions are outside my current skill level & project scope except as black boxes, but I'm loathe to turn off transactional fixtures because I want to keep everyone in my project using autotest and they have slow pcs :(

Can you let me know a version of edge that the tests in your patch pass against?

12/04/07 19:47:05 changed by lastobelus

I'm guessing the behaviour of destroy has changed since the patch was created.

(follow-up: ↓ 29 ) 12/04/07 19:52:47 changed by brian.wong

  • cc changed from bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer, lastobelus to bitsweat, jonathan.viney@gmail.com, airbaggins@users.sourceforge.net, gordon@expectedbehavior.com, tarmo, tolsen, lifofifo, jkraemer, lastobelus, brian.wong.

Hi, I've run into this problem with tests failing because transactions aren't nested. bitsweat, would you recommend not using transactions in unit tests?

(in reply to: ↑ 28 ) 12/04/07 20:04:46 changed by tarmo

Replying to brian.wong:

Hi, I've run into this problem with tests failing because transactions aren't nested. bitsweat, would you recommend not using transactions in unit tests?

I haven't tested this but I think you can disable transactions for a specific test with "uses_transaction :method_name".

From activerecord/test/associations_test.rb:

  uses_transaction :test_dependence_with_transaction_support_on_failure
  def test_dependence_with_transaction_support_on_failure
    ..
  end

Replying to lastobelus: I've not tested the patch for a long time as I've been quite busy and the and it's not easy to get a patch like this accepted, although I'm hoping it will eventually get accepted in some form.

The tests seem to only fail if you run all the tests at once, but not if you only run "ruby -I connections/native_postgresql transactions_test.rb". So I'm not really sure what the exact issue is. I'll see if I can find out.

(follow-up: ↓ 31 ) 12/04/07 20:32:41 changed by lastobelus

tarmo, we're having some trouble understanding how the patch is intended to work. Should it make tests with transactions with caught exceptions pass, or is it intended that we have to change our code to XXXX.transaction(:force => true) for tests with transactional fixtures & transactions to pass?

Currently our unit tests with transactions still fail with the patch, but pass if we change to using XXXX.transaction(:force => true) they pass, and we aren't sure if that is intended or if it is a result of the patch not working correctly with current edge rails.

(in reply to: ↑ 30 ) 12/04/07 20:54:47 changed by tarmo

Replying to lastobelus: I think the last patch is broken, not sure why. My first patch uses the Thread.current[:transactional_test] variable to decide if another transaction should be opened but the second patch for some reason is missing that part which is why your transactional tests don't get a nested transaction in the code that they test. You are right though that the intention is for the code inside the transactional test to get another nested transaction (which from the perspective of the code itself would be the first transaction as the tested code should not be aware of the surrounding transactional test's transaction).

12/04/07 21:56:56 changed by tarmo

  • attachment nested_transactions_with_implicit_transaction_config.3.patch added.

Restored the support for nested transactions in tests and fixed some assert_queries() which with the nested transactions support are getting two more queries for the transaction creation. The broken delete tests indeed appear two now have two more queries, unrelated to the nested transactions.

03/04/08 23:33:27 changed by MacStruan

Adding support for Oracle would be dead simple: it uses the same syntax as MySQL and Postgres for create_savepoint and rollback_to_savepoint. It doesn't support releasing savepoints, however.

05/09/08 04:53:07 changed by jonathan_viney

I've made a new plugin that works with the latest git version. Supports :force => true as an option to transaction to specifiy a savepoint, and fixes the issues with tests and transactional fixtures.

http://svn.viney.net.nz/things/rails/plugins/savepoints/

06/16/08 18:13:36 changed by sskirby