Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

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

Closed
wants to merge 1 commit into from

6 participants

@account-settings

... 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'

@NZKoz
Owner

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.

@NZKoz NZKoz closed this
@NZKoz
Owner

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?

@account-settings

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

@NZKoz
Owner

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.

@account-settings

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).

@NZKoz
Owner

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

@account-settings

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
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?

@account-settings

@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.

@mhfs

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

@jeremyf

@mhfs No responses for 2 months; close it.

@mhfs

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

@jeremyf

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

@rafaelfranca

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.

@NZKoz NZKoz closed this
@NZKoz
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
9 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -213,8 +213,13 @@ def transaction(options = {})
decrement_open_transactions
begin
if open_transactions == 0
- commit_db_transaction
- commit_transaction_records
+ if Thread.current.status != 'aborting'
+ commit_db_transaction
+ commit_transaction_records
+ else
+ rollback_db_transaction
+ rollback_transaction_records(true)
+ end
else
release_savepoint
save_point_records = @_current_transaction_records.pop
View
19 activerecord/test/cases/transactions_test.rb
@@ -619,3 +619,22 @@ def test_transaction_isolation__read_committed
end
end
end
+
+if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
+ class ThreadKillSafeTransactionTest < TransactionTest
+ def test_transactions_are_rolledback_when_thread_is_killed
+ dev = Developer.find(1)
+ original_salary = dev.salary
+ t = Thread.new do
+ Developer.transaction do
+ dev.update_attribute :salary, original_salary+50_000
+ sleep 10
+ dev.update_attribute :salary, 1 # never get here
+ end
+ end
+ sleep 2
+ t.kill
+ assert_equal original_salary, dev.reload.salary
+ end
+ end
+end
Something went wrong with that request. Please try again.