Problem/Motivation
In #843114: DatabaseConnection::__construct() and DatabaseConnection_mysql::__construct() leaks $this (Too many connections) we added \Drupal\Core\Database\Connection::destroy() but this introduces an unmanaged state of destroyed to a connection object that atm is completely untracked. This caused distractions while fixing #3126563: Replace usage of assertAttributeEquals() that is deprecated.
Proposed resolution
Use proper object destruction functions to result in reliable behaviour and simplify code.
Remaining tasks
Create change record
User interface changes
None
API changes
\Drupal\Core\Database\Connection::destroy() is deprecated.
Data model changes
None
Release notes snippet
@tdo
| Comment | File | Size | Author |
|---|---|---|---|
| #57 | 3128616-9.1.x-57.patch | 9 KB | alexpott |
| #57 | 55-57-interdiff.txt | 7.11 KB | alexpott |
| #55 | 3128616-9.1.x-55.patch | 17.56 KB | alexpott |
| #55 | 51-55-interdiff.txt | 801 bytes | alexpott |
| #51 | 3128616-9.1.x-51.patch | 8.9 KB | alexpott |
Comments
Comment #2
alexpottNote this functionality of closing connections is throughly tested in \Drupal\KernelTests\Core\Database\ConnectionUnitTest
Comment #3
alexpottMinor fix... now we're in a destructor this code is called in more situations therefore needs to ensure we have a PDO connection in the first place.
Comment #6
alexpottLocally
\Drupal\KernelTests\Core\Database\ConnectionUnitTestpasses fine once you add thegc_collect_cycles();maybe setting the PDO connection to NULL will help. I'm not sure why local and DrupalCI would behave differently at this point.Comment #7
alexpottSo the MySQL code that's already in destruct was bothering me. Because I can't see how it works if the connection is closed. And sure enough it doesn't. And the only reason we don't get errors is because we eat them in \Drupal\Core\Database\Driver\mysql\Connection::nextIdDelete().
The interdiff is the test-only patch.
Comment #9
daffie commentedComment #10
daffie commentedAdded something extra to the test to see if it works.
Comment #11
alexpott@daffie I really don't think we should be testing PHP internals here. We need to be testing that the connection is closed - not that garbage is collected. Also if we really have to test that then it belongs in ConnectionUnitTest and not NextIdTest test. But we already have coverage that the connection is indeed closed so I believe this is unnecessary.
Comment #12
daffie commented@alexpott In the patch you are adding a call to
gc_collect_cycles(). Should we not test that we have done so. When you remove the call togc_collect_cycles()in Drupal\Core\Database\Database::closeConnection() the test will fail.Comment #14
alexpott@daffie we are testing why we are calling gc_collect_cycles() - which is that the PDO connection is destroyed and the connection closed. That’s the purpose of the test. And yes if the call is removed the test fails so we have test coverage of our expectations.
Comment #15
alexpottWhat #10 proves is that there are some objects created that can be garbage collected after our call to gc_collect_cycles() which does not matter at all.
re-uploading #7 as that's the patch to review here. It'd be great to land #3128880: Make ConnectionUnitTest also run for PostgreSQL before this one as adding test coverage for psql to that test improves coverage - but it's not a blocker because the postrges driver does not implement destruct.
Comment #16
daffie commentedIs there any possibility that calling the function gc_collect_cycles() has a negative side effect for something else in Drupal? Personally I do not see any, but I think it is a question that should be asked.
Can we change this to: "Force the garbage collection to run. In that way PDO connection objects are really closed"
$loggerwith a link to Drupal\Core\Database\Log. Should that also not be set to NULL in__destruct()?Should
$this->connectionnot always be set to NULL, instead of now only when the if-statement is true?parent::__destruct()to \Drupal\Core\Database\Connection::__destruct(). According to the documentation on php.net: "Like constructors, parent destructors will not be called implicitly by the engine. In order to run a parent destructor, one would have to explicitly call parent::__destruct() in the destructor body." What does the default implementation ofstdClass::__destruct()do?Fix the @todo.
Comment #17
alexpott@daffie
Writing the answer to #16.3 and #16.4 made me realise that the default destructor is completely unnecessary now :) we can rely on PHP's object destruction and garbage collection explicitly rather than the current implicit reliance in HEAD.
Comment #18
alexpottSo there's a BC problem with this issue... because destroy() is no longer being called. But oddly no contrib is overriding this http://codcontrib.hank.vps-private.net/search?text=public+function+destr... as far as I can see.
Comment #19
daffie commentedWill do a full review after the testbot returns green.
Nitpick: Fix the @todo.
Comment #20
alexpott@daffie nice spot because of #19 it won't be green :)
Comment #22
alexpottAnd here's a proper BC layer were object destruction will call Connection::destroy() and a deprecation will be triggered if the contrib or custom connection class is implementing destroy but not if core does. No concrete core connection implements this anymore so we're good. In Drupal 10 we can remove \Drupal\Core\Database\Connection::destroy() and \Drupal\Core\Database\Connection::__destruct()
Comment #25
alexpottHmmm this is super odd. The test passes locally and not on DrupalCI. Which implies that somewhere DrupalCI has a reference to the database connection object that is not present when running the test locally.
Comment #26
mondrake#25 a PDOStatement object left open after a PDO::prepare, somewhere, maybe?
Comment #27
alexpottFixed this once before in #6. Ho hum. Seems we can't quite remove
\Drupal\Core\Database\Connection::__destruct(). But that's no biggie. At least we still get the more predictable behaviour of using a proper destructor and we fix the MySQL destructor to actually work if closeConnection is called.Comment #28
mondrakeLovely! Let's make objects do OOP work! LGTM
Nit:
maybe we can use
StubPDO::classinstead of FQCN here?Comment #29
xjmI half-remember from very early D8 development that we had a reason for avoiding using
__destruct(), but I don't remember what it was. The only clue I found was incore/lib/Drupal/Core/Lock/DatabaseLockBackend.phpwhich alludes to a problem with garbage collection, so maybe it is not relevant anymore on more modern PHP versions? We have very few destructors in core, but more than 0.Comment #30
xjmAlso dredged up #1891980: Add Service termination API to reliable terminate/shut down services in the container from less early D8 development.
Comment #31
xjmFrom #1811730-14: Refactor Database to rely on the DIC solely:
Comment #32
xjmI also want a "Needs Sascha review" tag.
Comment #33
berdir> #1811730: Refactor Database to rely on the DIC solely
Oh, the dark and sad memories.
Has been a very long time since I worked on that. I doubt that newer PHP versions changed what I said back then, but the problems there might have been specific to when the connection is in the container And I can see that there are some workarounds to solve circular references in the patch now, so maybe @alexpott just figured out what I couldn't back then.
countQuery() is quite inefficient as it does a SELECT COUNT(*) from (inner query), directly doing a "$query->addExpression('COUNT(*)')" is more efficient, but doesn't really matter here and for this query. (countQuery() is built to be reliable with complex queries, not fast)
Don't we have a soft requirement to put more than 2 chained calls on separate lines though?
Comment #34
ghost of drupal pastThe change record should mention custom database drivers need to call parent::__destruct now (? or is that only for D10 and not doing so is merely deprecated for now? needs to clarify this).
Comment #35
xjmBetter status.
Comment #36
mondrakeI cannot understand what is still open here, sorry. This patch improves DX to orderly shutdown db connections, which otherwise now requires balancing what needs to be done in destroy() and what in __destruct(), and that feels fragile and obscure. Let’s kill destroy that smells like procedural legacy.
Edit: see also the problems that this generated in #3126563: Replace usage of assertAttributeEquals() that is deprecated
Comment #37
alexpott@mondrake we need to update the issue summary and the change record with the details. I agree that the patch is largely ready but the more eyes on the BC layer the better because these things are not easy. So I’m going to set to needs work to get the issue metadata updates done. (Will try to do them later if I have time)
Comment #38
mondrakeI drafted the CR, please review.
Also, tested this patch with the DruDbal experimental driver. PR #179 is passing - that applies this patch and a cleanup of the driver to fix HEAD there that is failing since #3128880: Make ConnectionUnitTest also run for PostgreSQL.
Comment #39
ghost of drupal pastThanks, it is very clear to me. Perhaps change the title "\Drupal\Core\Database\Connection::destroy has been replaced by __destruct()". Most change record titles I can remember describe what happened. Also note even if we don't change to that, destruct has a typo in the title.
Comment #40
alexpott@Charlie ChX Negyesi I've updated the CR title.
Comment #41
daffie commentedThe test fails when I change the line to:
$fake_connection->__destruct();The error that I get is:
We have
!=and!==on the same line. Should we not use!==for both?The nitpick from comment #28 is still unaddressed.
The BC layer looks good to me, only I am not a 100% convinced that it will work in all circumstances. I am very happy with the tag "Needs framework manager review".
Comment #42
alexpottRe #41.1 you can't call destruct() like that. It's a magic method. You end up not destroying the object. It's just not supported like that. Should only be called by the garbage collector when their are no more references.
Patch fixes the nits.
Comment #43
daffie commented@alexpott: Thank you for explanation for #41.1.
All remarks are addressed.
All code changes look good.
Changing the status to RTBC for the framework managers review.
Comment #44
mondrakeActually #42 doesn’t address #28, since another prexisting method is changed, not the one introduced by the patch itself. Superminor though...
Comment #45
daffie commentedFixed the nitpick from comment #44.
Leaving the status to RTBC, because the change is very small. Feel free to change the status when you do not agree with me.
Comment #46
alexpottLet's undo the unrelated change then. It's fine changing the new test to use ::class but if we change the existing tests then we should change them all. And that's out-of-scope.
Comment #47
mondrakeComment #48
daffie commentedStraight reroll.
Comment #49
catchI think we need a @todo on ::__destruct() to say exactly what needs to happen to it in 10.0.x. It's not clear to me reading the patch.
Comment #50
alexpottGood idea. The thing to do in 10.0.x is to remove the call to $this->destroy();
In some ways this is covered by
Because once we remove that implementation tests will start failing. But there's no harm in being explicit.
Comment #51
alexpott@todo and D10 issue added - #3153864: Remove \Drupal\Core\Database\Connection::destroy() BC layer
As this is only a comment set back to rtbc.
Comment #52
catchOK the reason I asked is because the logic in $this->destroy() is different to the logic in __destruct(), so it wasn't clear whether it needed to be copied over prior to removal or not.
Comment #53
alexpottIt doesn't need to be copied.
This logic is not necessary once we move to a proper destructor. It's not even running for core db drivers anymore and will only run if a custom or contrib driver has a destroy method - to keep up BC.
Comment #54
catchThis is the kind of explanation I was hoping we could add to the comment.
Comment #55
alexpottAdded this information to the @todo
Comment #56
catchThis is from a different patch.
Comment #57
alexpottDoh... didin't rebase properly. The interdiff shows everything that's not supposed to be in the patch being reverted...
Rebased my locally branch properly.
Comment #59
catchCommitted 2ee59be and pushed to 9.1.x. Thanks!
Comment #61
quietone commentedPublish CR