-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decouple transactional fixtures and active connections #50999
Conversation
c52280f
to
58a01b8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the conceptual inversion here... pool.lock_thread = true
always felt like it made the approach sound hackier than it is, even if imperfect.
connection = checkout_and_verify(acquire_connection(checkout_timeout)) | ||
connection.lock_thread = @lock_thread | ||
connection | ||
return @pinned_connection if @pinned_connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have mixed feelings on a pin overriding an explicit checkout 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't have it initially, I can try removing it, but I think it was necessary to pass some tests (I've been fighting with some state leaks in test that predate my change but are made more apparent now).
But overall this "pin" feature is really meant as a private API for transactional fixtures only. I'm even tempted to look at instead replacing the connection pool by a different implementation when you use transaction fixtures.
But it's already the third major refactoring I'm doing to get rid of the checkout cache, so I'm not super keen on going deeper in the rabbit hole...
def unpin_connection! # :nodoc: | ||
raise "There isn't a pinned connection" unless @pinned_connection | ||
|
||
yield @pinned_connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This yield feels a bit weird... it's strictly safer than callers assuming they hold a pin, doing things, then hitting the raise... but maybe that's okay / their problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not too sure what you mean. The raise is mostly here for me to find issues with the change (and it did find some). Since this is an internal only API it's really meant as some form of assertion. If you unpin twice, something weird likely happened.
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
Outdated
Show resolved
Hide resolved
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
Show resolved
Hide resolved
58a01b8
to
27a7f5f
Compare
For tomorrow:
|
Alright, I think I got rid of the leaks by detecting when the transaction was committed and properly reseting state when that happen. This is likely a very good thing even for apps using this, it just need to be cleaned up. The |
Alright, the stuck test is: What is happening is that it sets up: require "test_helper"
class ParallelTest < ActiveSupport::TestCase
Q1 = Queue.new
Q2 = Queue.new
test "one" do
assert_equal "x", Q1.pop # blocks until two runs
Q2 << "y"
end
test "two" do
Q1 << "x"
assert_equal "y", Q2.pop # blocks until one runs
end
end Which dead locks because only one thread can reasonably have transactional fixtures enabled (and realistically do DB operations on the same DB). So the test was passing before, because the "pinned connection" (the active one really) was the only pinned one. Now we essentially lock the whole pool. The doc states:
So not quite sure what to do here, I'll think about it. Either way I think there a bunch of things I can cleanup and extract from this PR, starting with the leak handling etc. |
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
89be1c7
to
b6cd3a2
Compare
ca426d4
to
b8c1bd0
Compare
2c503fc
to
b34ba9f
Compare
Alright, I finally figured the last flaky test, mostly need to cleanup now, and add some coverage. |
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 - Make fixtures setup and teardown methods private. - Don't run adapter thread safety tests with sqlite3_mem - Make foreign_key_tests more resilient to leaked state - Use `exit!` in fork to avoid `at_exit` side effects. - Disable transactional fixtures in tests that do a lot of low level assertions on connections or connection pools.
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 - Make fixtures setup and teardown methods private. - Don't run adapter thread safety tests with sqlite3_mem - Make foreign_key_tests more resilient to leaked state - Use `exit!` in fork to avoid `at_exit` side effects. - Disable transactional fixtures in tests that do a lot of low level assertions on connections or connection pools.
Extracted from: rails#50999 Some tests may use the connections in ways that cause the fixtures transaction to be committed or rolled back. The typical case being doing schema change query in MySQL, which automatically commits the transaction. But ther eare more subtle cases. The general idea here is to ensure our transaction is correctly rolling back during teardown. If it fails, then we assume something might have mutated some of the inserted fixtures, so we invalidate the cache to ensure the next test will reset them. This issue is particularly common in Active Record's own test suite since transaction fixtures are enabled by default but we have many tests create tables and such. We could treat this case as an error, but since we can gracefully recover from it, I don't think it's worth it.
Extracted from: rails#50999 - Make fixtures setup and teardown methods private. - Don't run adapter thread safety tests with sqlite3_mem - Make foreign_key_tests more resilient to leaked state - Use `exit!` in fork to avoid `at_exit` side effects. - Disable transactional fixtures in tests that do a lot of low level assertions on connections or connection pools.
@casperisfine Hey! I'm super late to the party, but somehow I missed this change :( (Don't want to open an issue/PR without discussing first, so...) Context: this change breaks TestProf's before_all (used by tons of apps including, e.g., Discourse) in a way that it would be hard and unpleasant to fix without a decent amount of monkey-patching. Sure, I can do that but maybe it's not too late tweak the implementation. What are the concerns? First, it's a bit surprising (to me) that we hid Although pinning is only used in test_fixtures.rb now, I'm not sure that making it transactional by default (and without an option to avoid transactions) would make senses in other scenarios (though I can't imagine one). My proposal is to move Secondly, I would propose making pinning idempotent, i.e., calling I'm happy to work on this changes if there is a green light from the team. |
If you don't need a transaction, you don't need pining at all, or am I missing something? |
Yeah, at least in the known scenarios ( The problem is with nested transactions. Prior to this change, pinning management and transactions management were independent, so we could pin connections and start transactions in any order, and, which is more important, double-pinning wasn't a problem. We had:
With before_all (context-wide transaction):
Now, we cannot simply |
I'm sorry, I still don't understand why TestProf would need to concern itself with connection pinning. I'll try to read the code tomorrow. |
Is this the reason? |
(Oops, I was wiring my long response...). Yeah, it all goes down to threads. And of the potential causes of threads involved into the setup phase is AJ async (via AR, callbacks, and all that typical Rails stuff). |
I forgot to ask for some repro steps so I could see if there was a better solution in test-prof -_-. I found the test case that does Active Record queries from a background thread in So I'm wondering if what you need isn't just a way to trigger db setup early? |
My gut feeling is that you basically want to lift If double-pinning was the only concern, I think you could amend your previous timeline by inserting an
That doesn't feel great to me, conceptually, but given the fact unpins already occur between the rest of the tests after the first one, it doesn't seem newly bad. (From a quick re-read of Getting beyond what's practical for 7.2 right now, I do wonder if there's a worthwhile object to be teased out here, separating the shared-connection management from the actual fixture population/retrieval. It seems like it would be pretty useful for you to have an easy place to patch in to the subscriber, for example, to avoid your "need to ensure the connection classes are loaded" instruction. |
Good catch. I've updated the test to fail and better demonstrate the problem with no |
Right, so I think this better show that what you are really after it to trigger fixtures insertion / connection pinning earlier. So perhaps that's what we should provide? |
Currently, What I can probably do is mark the connection as unpinned (like, remove the instance variable and let the
Yeah, looks like a bug if we assume that pinning can be used with outer transactions. |
Yeah, kind of. And I still need the current transaction-wrapping done by fixtures. P.S. Added threaded scenarios to the fixtures scenario: test-prof/test-prof@8fe54fa (currently playing with it and the current Rails |
Oh right 🤦 -- ummm... start an extra layer of transaction right before you unpin? 😅 |
@palkan, about the initial refactoring you suggested, I don't quite pitcture it myself at this stage, but I'm not hell bent on pinning and the begin_transaction being tied together. So if you think you can come up with a refactor that makes your life easier without making the Rails side worse, I'm totally open to merge it. As for why pin/unpin raise an exception if called twice, it's because during development I ran into issues caused by the code being called multiple times, so if you want to remove that, we'll need some tests that asserts it's behaving as expected when used "recursively". |
That's what I ended up with—removing
👌 |
Ref: #50793
Transactional fixtures are currently tightly coupled with the pool active connection. It assumes calling
pool.connection
will memoize the checked out connection and leverage that to start a transaction on it and ensure all subsequent accesses will get the same connection.To allow to remove checkout caching (or make it optional), we first must decouple transactional fixtures to not rely on it.
The idea is to behave similarly, but store the connection in the pool as a special "pinned" connection, and not as the regular active connection.
This allows to always return the same pinned connection, but without necessarily assigning it as the active connection.
Additionally, this pinning impact all threads and fibers, so that all threads have a consistent view of the database state.
FYI @matthewd