Skip to content

Do not attempt to return connection with open transaction to pool#24610

Merged
sgrif merged 1 commit into
rails:masterfrom
sgrif:sg-checkin-transactions
Apr 22, 2016
Merged

Do not attempt to return connection with open transaction to pool#24610
sgrif merged 1 commit into
rails:masterfrom
sgrif:sg-checkin-transactions

Conversation

@sgrif

@sgrif sgrif commented Apr 18, 2016

Copy link
Copy Markdown
Contributor

When the query cache completes, if Active Record is still inside of a
transaction, it is because the transaction is meant to be left open
above this unit of work (such as transactional fixtures in tests). There
were several tests around the behavior of "tests" that were invalid, as
tests are not run through the executor. They have been changed to
reflect the new behavior, which is closer to what actually occurs in
Rails tests.

Fixes #23989
Fixes #24491
Close #24500

When the query cache completes, if Active Record is still inside of a
transaction, it is because the transaction is meant to be left open
above this unit of work (such as transactional fixtures in tests). There
were several tests around the behavior of "tests" that were invalid, as
tests are not run through the executor. They have been changed to
reflect the new behavior, which is closer to what actually occurs in
Rails tests.

Fixes rails#23989
Fixes rails#24491
Close rails#24608
Close rails#24500
@sgrif

sgrif commented Apr 18, 2016

Copy link
Copy Markdown
Contributor Author

r? @matthewd
/cc @jeremy @rafaelfranca @arthurnn

@matthewd

Copy link
Copy Markdown
Member

My concern with this is that it means we'll leak connections if (for whatever reason) there's a transaction left open. Obviously returning them to the pool is also unwise, but IMO we should more likely be raising, or discarding the connection, if we "finish" with it dirty.

To me #24608 is more correct, both because the test is a unit of work, and most critically because the test body is user code that can trigger a load -- without it, we lose thread safety.

@sgrif

sgrif commented Apr 18, 2016

Copy link
Copy Markdown
Contributor Author

The only way that a transaction would be opened (in a way that transaction_open? can detect) should be through ActiveRecord::Base.transaction which rolls back or commits in an ensure block. Even if we did somehow leak one, it would get reaped once the thread finishes regardless.

Additionally, unless I'm missing something, this code would be extremely broken if the transactions were leaking, as PG is the only backend which overrides reset! to clear transactions properly. All of the others would return to the pool with the transaction still open.

@sgrif

sgrif commented Apr 18, 2016

Copy link
Copy Markdown
Contributor Author

Perhaps another approach might be to check if the connection is established when the unit of work begins, and only return it if the connection was established as part of this work. But more generally my concern is that this code is making some major assumptions about the ownership of the connection, with no verification that they are true. And env["rack.test"] is a case where its assumptions don't hold, but I don't think it's the only case

ActiveRecord::Base.clear_active_connections!
unless ActiveRecord::Base.connection.transaction_open?
ActiveRecord::Base.clear_active_connections!
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be more like this, as ActiveRecord::Base.clear_active_connections! will clear current thread connection on all connection pool

ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
   pool.release_connection unless pool.active_connection? && pool.connection.transaction_open?
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the discussion here is about the ownership of the connection for the current thread, and whether we should try to release it, the pool shouldn't be involved at all here. Clearing unused connections from other threads is a side effect of checking the current connection in, not the main goal.

@mfazekas mfazekas Apr 20, 2016

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sgrif my code should not be deal with other thread's connections. It tries to deal with current thread's connections on multiple connection pools.
In other words it's about multiple connections (to different databases) of the current thread - maybe only some of them has a transaction opened.

@mfazekas

Copy link
Copy Markdown
Contributor

I think #24608 is neccesary, and yes ideally this one would verify the assumptions about connections at the run callback, but it might be better to raise if the assumption is not correct. That is likely a result of a configuration error - user should add outer executor vs a runtime error.
FYI there is also a PR at lower level to raise if we attempt to checkin a connection with opened transactions on it: see https://github.com/rails/rails/pull/24493/files

@sgrif

sgrif commented Apr 19, 2016

Copy link
Copy Markdown
Contributor Author

So after thinking about it a bit more, I'm more convinced that this is the right way forward. Ideally our semantics would instead be something like: "check if a connection is already established when the unit of work begins. If not, establish one, and return it to the pool at the end of the unit of work". However, I think that would be much more likely to leak connections that this path is, simply because a middleware somewhere might establish a connection before we get here without realizing the implications.

Unfortunately, I think this is just a side effect of the fact that the query cache held the responsibility for managing the connection pool, rather than something more explicitly meant to keep that role. Given that we haven't had clear semantics on the ownership roles at play here, any significant changes would likely cause subtle breakage.

As such, I think dealing with a transaction being open as our main indicator is both closest to the previous condition of env["rack.test"] being present, but also most closely tied to the semantics that we're looking for.

@sgrif

sgrif commented Apr 19, 2016

Copy link
Copy Markdown
Contributor Author

@matthewd WDYT re ---^?

@matthewd

Copy link
Copy Markdown
Member

I did come close to implementing it that way ("leave the connection in whatever state it was when we started")... the reasons I didn't were twofold: 1) the danger that something else makes the connection before we run, as you've described, and 2) I realised the test already needed to be wrapped by the executor for thread safety, which obviated this behaviour.

So, I don't feel strongly that we shouldn't make the change you're describing... but I do believe it only solves one of two problems.

@sgrif

sgrif commented Apr 22, 2016

Copy link
Copy Markdown
Contributor Author

Going to go forward with this, as I think we've addressed the only concern with this specific change. #24608 is likely a change we should make in addition to this.

@sgrif sgrif merged commit 1ca6f7f into rails:master Apr 22, 2016
@mfazekas

Copy link
Copy Markdown
Contributor

@sgrif i don't think my comment regarding multiple database connections was addressed, was it? #24610 (comment)

As a concrete case: If you have the default connection to sqlite and another to postgresql, but you only have fixtures for classes stored in postgresql, then no transaction will be opened on sqlite - ActiveRecord::Base.connection. So this code will put the psql connection with opened transaction back to the pool. Shall i write a testcase demonstrating it?

@sgrif sgrif deleted the sg-checkin-transactions branch April 22, 2016 19:46
@sgrif

sgrif commented Apr 22, 2016

Copy link
Copy Markdown
Contributor Author

If it can be demonstrated through the public API only, then yes. That doesn't sound like something that can occur in a normal Rails app, though.

@mfazekas

Copy link
Copy Markdown
Contributor

@sgrif so this one reproduces deadlock using public API but with multiple db connections:
https://gist.github.com/mfazekas/55ba95e52d4959bfca94787481313ca7

I agree it's not a normal rails app, but i don't think it deserves this kind of punishment.

I was wrong about rails fixtures, as enlist_fixture_connections actually returns all the connections, irrespective where we have fixtures (which sounds correct), so with fixtures this cannot be reproduced, as fixtures puts a transaction on all connections.

@ojab

ojab commented Apr 25, 2016

Copy link
Copy Markdown
Contributor

#24004 is also fixed by this PR, should it be closed or it's better to wait for #24608 and recheck?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction deadlock with rails 5 beta 3 Delivering mail causes tests to fail

5 participants