Skip to content
This repository

transactions are rolled back if thread is killed during transaction #382

Closed
wants to merge 1 commit into from

7 participants

coderrr Michael Koziarski Jeremy Kemper Marcelo Silveira Jeremy Friesen Rafael Mendonça França David Rawk
coderrr
coderrr commented

... as opposed to being partially committed. Only fixes behavior in MRI 1.8 and Rubinius. JRuby and 1.9 will still work the same since their Thread.current.status won't be 'aborting'

Michael Koziarski
Owner
NZKoz commented

Thread#kill and Thread#raise are basically impossible to use in a safe and reliable manner because the exceptions can come from anywhere and be of any type, Charles from jruby had a great write up about it several years ago. For example your particular patch won't help if the exception comes inside the commit_db_transaction method itself.

I think attempting to be well behaved in the presence of Thread#kill is going to be too much effort to be worthwhile.

Michael Koziarski NZKoz closed this
coderrr coderrr reopened this
Michael Koziarski
Owner
NZKoz commented

I realise that, however my point remains, it's essentially impossible to have deterministic code in the presence of Thread#kill, your code should never call it.

Why were you calling it?

coderrr
coderrr commented

Did you read the post? It pretty much explains the issue exactly. tldr is all threads are Thread#killed when a process terminates.

Michael Koziarski
Owner
NZKoz commented

Indeed I did read the post, thanks for asking :P What I didn't see is why you didn't register a TERM handler?

My objection remains all the same, #kill and #raise can arise basically anywhere and I'm not sold that having a single potential case addressed with code that only runs on a subset of our supported runtimes is justified here given the literally endless number of cases which can be result from the methods in question.

coderrr
coderrr commented

K so firstly I did mention that a signal handler is what you should do and mitigates the issue. Of course if something goes wrong in your signal handler you end up getting Thread#killed again.

Secondly, this isn't a solution for Thread#raise at all, only Thread#kill.

Thirdly and most importantly, this makes sure you will NEVER get a partially committed transaction due to a Thread#kill. Doesn't matter where the interpreter is when it's called. Of course, this is only true on 1.8 and Rubinius unless the others also change their thread status behavior (which I think they probably should).

Michael Koziarski
Owner
NZKoz commented

My next suggestion was going to be to refactor the change so that instead of changing here it's in commit_db_transction (it could simply raise if the thread is aborting). However that would prevent you from deliberately commiting transactions in ensure blocks when the thread is being killed.... In fact doesn't your code here prevent people from committing in ensure blocks?

I can definitely see your point, but a partial fix for a subset of interpreters isn't really good enough. I'll see if I can ping the jruby guys to get their thoughts on the change and whether they intend changing their code's behaviour

coderrr
coderrr commented

Yea it'd be great if you could try to get 1.9 and JRuby to implement matching Thread.current.status behavior.

My code only prevents transactions from being committed when the Thread is being killed. This is how it should be because if the Thread is being killed you have no way of knowing whether the transaction was completed or not (aside from the local var / flow control stuff I talked about in the post).

The only issue I see with raising in commit_db_transaction if the Thread is being killed is that (unless you catch that exception) then you won't explicitly call rollback. But maybe that isn't an issue.

Jeremy Kemper
Owner

This is a reasonable concern, but I think the platform-specific fix isn't going to fly since everyone's moving to 1.9.x where it doesn't even work.

Is there a better way? How about the local variable approach in your blog post?

coderrr

@jeremy, not that I could figure out when I was researching this. The local variable approach will roll back any transaction that uses the flow control statements I mention in the post. I think the only hope is to get 1.9 and jruby to set Thread#status to 'aborting' consistently.

Marcelo Silveira

@jeremy @coderrr ping - this one seems to loose relevance with rails 4 being 1.9 only. wdyt? let's close it?

Jeremy Friesen

@mhfs No responses for 2 months; close it.

Marcelo Silveira
mhfs commented

@jeremyf I don't have permission to close it...

Jeremy Friesen

@mhfs sorry about that…@coderrr any follow-up?

Rafael Mendonça França
Owner

I can close, but I guess we should not. I think that issue is still there either with Ruby 1.9. I'm asking to the core team.

Michael Koziarski NZKoz closed this
Michael Koziarski
Owner
NZKoz commented

Closing as we only support 1.9 now and the proposed changes here don't work there.

David Rawk

Shouldn't the ensure be all rollback too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

May 03, 2011
coderrr transactions are rolled back if thread is killed during transaction 7f8ac26
This page is out of date. Refresh to see the latest.
9  activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -213,8 +213,13 @@ def transaction(options = {})
213 213
           decrement_open_transactions
214 214
           begin
215 215
             if open_transactions == 0
216  
-              commit_db_transaction
217  
-              commit_transaction_records
  216
+              if Thread.current.status != 'aborting'
  217
+                commit_db_transaction
  218
+                commit_transaction_records
  219
+              else
  220
+                rollback_db_transaction
  221
+                rollback_transaction_records(true)
  222
+              end
218 223
             else
219 224
               release_savepoint
220 225
               save_point_records = @_current_transaction_records.pop
19  activerecord/test/cases/transactions_test.rb
@@ -619,3 +619,22 @@ def test_transaction_isolation__read_committed
619 619
     end
620 620
   end
621 621
 end
  622
+
  623
+if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9' 
  624
+  class ThreadKillSafeTransactionTest < TransactionTest
  625
+    def test_transactions_are_rolledback_when_thread_is_killed
  626
+      dev = Developer.find(1)
  627
+      original_salary = dev.salary
  628
+      t = Thread.new do
  629
+        Developer.transaction do
  630
+          dev.update_attribute :salary, original_salary+50_000
  631
+          sleep 10
  632
+          dev.update_attribute :salary, 1 # never get here
  633
+        end
  634
+      end
  635
+      sleep 2
  636
+      t.kill
  637
+      assert_equal original_salary, dev.reload.salary
  638
+    end
  639
+  end
  640
+end
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.