Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
This issue is related to how the database connection is being destroyed. Seems like PHP8.0 does not like the D7 implementation, i.e. either the internals of PHP has changed or PDO exceptions are not silenced any more (more likely)
Steps to reproduce
1. Apply patch from the meta issue
2. Run tests with PHP8.0
Proposed resolution
Backport the solution from D9
Remaining tasks
Provide a patch & review
User interface changes
None
API changes
None
Data model changes
None
Release notes snippet
None
Comment | File | Size | Author |
---|---|---|---|
#30 | 3200708-30.patch | 1.73 KB | mcdruid |
#30 | interdiff-3200708-27-30.txt | 915 bytes | mcdruid |
#27 | 3200708-27.patch | 1.35 KB | mcdruid |
#26 | 3200708-26.patch | 498 bytes | mcdruid |
#11 | 3200708-11.patch | 3.36 KB | mcdruid |
Issue fork drupal-3200708
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mcdruid\ConnectionUnitTest::testOpenClose
and others in the same test class fail when they doassertNoConnection($id)
.Looks like that's because garbage collection doesn't destroy the connection properly unless we ensure that the
connection
is null.D9 does that here: https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/lib/Drupal/C...
...we could copy the same (add it to
\DatabaseConnection::__destruct()
) or we could add it to\DatabaseConnection::destroy()
.The latter would be my preference:
However, you could argue it'd match D9 more closely to do the former. I don't think there's much difference either way, but the comments etc.. match nicely in
destroy()
without having to add more explanation of what's going on.Comment #4
mcdruidActually doesn't look like it makes sense to set the connection to NULL there straight after calling
setAttribute()
on the connection?In the particular case of the failing unit tests, I don't think it makes much difference but perhaps we should stick to copying D9 and add it to
\DatabaseConnection::__destruct()
.Comment #5
Taran2LComment #6
mcdruidLooks like gitlab can't rebase the MR itself.
Is this the new equivalent of a "needs reroll" then?
I also don't see the fix outlined in #3/#4 in the MR?
Comment #7
Taran2LAnd it does not work :)
Because the DrupalCacheArray class writes to the DB during the destruction, see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/bootstrap..... Sometimes, the connection object is not there anymore, as there is no control over when __destruct() gets called.
D8 has fixed this a long time ago #1891980: Add Service termination API to reliable terminate/shut down services in the container
Comment #8
mcdruidHmmz.
Just to confirm that I get the same result moving this to
__destruct()
instead (like D9, per #4):...which isn't really surprising.
CommentActionsTestCase
seems to be a convenient test to reproduce the issue.Comment #9
mcdruidThis is looking somewhat tricky to me.
The typical error is:
This error goes away if we don't set the PDO connection to null explicitly in either
\DatabaseConnection::__destruct()
or\DatabaseConnection::destroy()
.However, if we don't do
$this->connection = NULL;
(which D9 does in the connection's__destruct()
) the PDO connection will not be garbage collected and tests such as\ConnectionUnitTest::testOpenClose()
will fail.In addition to failing tests, there's presumably some risk of connections piling up in real use - see comments such as:
We can't catch the
PHP Fatal error: Uncaught Error: Call to a member function ... on null
in older versions of PHP at least so something like this won't work (in\DrupalCacheArray::__destruct()
):...there seems to be some precedent for doing something like that e.g. in
ViewDataCache
's destructor before #1891980: Add Service termination API to reliable terminate/shut down services in the container was committed, but it doesn't look like it'd help here.We probably don't want to introduce checking for the presence of the database connection in the calling code; the error's actually happening when
lock_acquire()
tries to access the semaphore table before doing acache_set()
but both the lock and cache systems are "pluggable" and may have nothing to do with the database in many cases.No solution yet, but wanted to scribble down some notes after looking into this a bit.
Comment #10
Taran2LThe workaround I was able to come up with is to use
register_shutdown_function()
over the__destruct()
. This might be considered an API break so requires few more eyes.Is it possible to check contrib for extending
DrupalCacheArray
?Seems like PHP5.x does not do garbage collecting properly (at least with MySQL 5.5). Needs further investigation.
Comment #11
mcdruidSorry to go back to a patch, but I'm still getting familiar the MR workflow...
I thought about a shutdown function, and that might indeed be useful.
Another way to avoid the error is something like this patch where we tweak things to make the exception catchable.
I'm not certain it's a great idea, but want to test with the different PHP versions.
If we went down this road, we'd probably want to do something like proxy the PDO object so that we can check it's actually still there before calling any of its methods - here we're just doing that specifically for
prepare
.I'm not entirely sure what the consequences of the call to
set($data)
failing in\DrupalCacheArray::__destruct
might be. If we're effectively accepting data loss that's obviously not good.Giving this approach a quick try anyway..
Comment #12
Taran2Lhi @mcdruid, you can open a new fork and work with it VS plain text patches.
Seems like your solution results in the same garbage collection issue with PHP5.x as mine, except mine (probably) writes this cache data to the DB on PHP process shutdown
Comment #13
mcdruidNoting that I think we avoid the original "Error: User-supplied statement does not accept constructor arguments in PDO->prepare()" issue if we just don't set this attribute (in
destroy()
):With that commented out e.g.
CommentActionsTestCase
will pass without the problem triggered by theDrupalCacheArray
destructor.However, it looks like other tests will then fail - e.g.:
...which is probably because the connection's not being garbage collected properly (although I've not dug into the details there).
I am wondering what's the smallest possible change we can make to get PHP 8 passing without breaking PHP 5...?
Comment #14
mcdruid...on the other hand it seems very broken that the
DrupalCacheArray
destructor is apparently writing to the db after the connection's been closed / destroyed (or at least trying to).I'm not sure we can backport the D8/9 improvements for that without some really drastic changes.
I wonder if it would matter if e.g. the ThemeRegistry is not written back to (database) cache if that happens after the db connection has been torn down.
There are other places where we seem to shrug this sort of thing off e.g. in
\DrupalDatabaseCache::set
there is:So perhaps something like #11 might be viable?
Comment #15
mcdruidFWIW, with the latest patch from #3204161: [D7] MySQL on PHP 8 now errors when committing or rolling back when there is no active transaction all Database tests pass for me apart from these 2:
...which both blow up with messages like this:
I suspect those two will pass when we fix this issue.
Comment #16
mcdruidCautiously optimistic that avoiding messing with the PDO statement class in
\DatabaseConnection::destroy()
avoids the problem we're trying to address here, without breaking garbage collection / closing connections for PHP 8.I don't think the Database tests will pass 100% without #3204161: [D7] MySQL on PHP 8 now errors when committing or rolling back when there is no active transaction but I think it may only be the transaction tests from that issue that are still a problem.
Comment #17
mcdruidNope, I still had the try/catch in DrupalCacheArray's destructor from #11 when I ran tests locally.
This is still two steps forwards and one step back unfortunately.
Comment #18
mcdruidLinking the issue which added the connection unit tests and a lot of the code we're looking at which allows the PDO connections to be closed.
Comment #19
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedHere is the plan how to fix:
- DrupalDatabaseCache gets the Database::connection() injected into `$this->connection`
- Add setConnetion / getConnection() methods, too
- DrupalCacheArray injects the cache object (so it does not get destroyed while it's still using it)
- We make a cache_get_object() method public that calls _cache_get_object() (private)
- We don't add __destroy as its a breaking change
Comment #20
mcdruidDiscussed this with @Fabianx who suggested a different approach, along the lines of using dependency injection to prevent the database connection object from being destructed too early.
We also agreed to try and keep the changes to a minimum in order e.g. not to break PHP 5 (which D8/9 didn't have to worry about) and also any contrib db drivers (note that earlier backport patches meant the driver classes had to call
parent::__destruct();
).Here's a simple patch which I think makes the main changes we discussed. No interdiff as wouldn't be useful.
This seems to address the PHP8 problem with
DrupalCacheArray
's destructor trying to access the db connection after it's been torn down in the test I've been using to reproduce that.However, we're now hitting different problems:
I wonder if we need to implement a destructor instead of calling
\DatabaseConnection::destroy()
from\Database::closeConnection()
.Comment #21
mcdruidNR for the testbot
Comment #22
Taran2LI was playing with it for a few hours and still cannot figure out why garbage collection is not being forced. The test code is fairly simple, don't see any loose ends ..
Comment #23
Taran2LSo, I guess this script shows the issue: https://3v4l.org/1vSZY
Comment #25
Taran2Lhi @mcdruid, I went ahead and committed your patch to your issue fork (it was empty anyway). Hope you don't mind.
Seems like, hm ... cache bins are being inited very early and the database might not be available yet.
Comment #26
mcdruid@Taran2L no problem committing to that fork - I will try to get used to using that / MRs instead of patches but prefer to concentrate on getting fixes ready for the release next week.. that probably means old-school patches for a little bit longer :)
When we discussed this earlier @Fabianx asked an important question about what's calling the code which destroys/breaks the db connection, and having dug into that a bit more I think we may be facing a problem created by the way our tests run.
https://git.drupalcode.org/project/drupal/-/blob/7.78/modules/simpletest...
\DrupalWebTestCase::tearDown()
does:...and AFAICS it's that call to
removeConnection()
which callsself::closeConnection()
which calls\DatabaseConnection::destroy()
which seems to happening just before things go boom.As a quick experiment, what happens if we don't make that call to
closeConnection()
? So far in local testing I'm not seeing any failures with PHP 8 or PHP 7.4, includingConnectionUnitTest
which checks that connections are closed properly.Maybe we'll need to do something a bit more nuanced, but let's try just commenting that one line out and running tests...
Comment #27
mcdruidI don't know whether to laugh or cry :)
Without that call to
closeConnection()
PHP 8.0 tests run properly (remaining failures are not related AFAICS), and all the other PHP versions still pass.Looks like the only other place in core that calls
Database::removeConnection()
isDrupalUnitTestCase::tearDown()
.It's public though.
How about adding an optional param for whether to close the connection, which defaults to TRUE.
We'll leave the other call in
DrupalUnitTestCase::tearDown()
alone "if it ain't broke.."If results are the same for this patch as for #26 hopefully this is an unexpectedly simple answer to my earlier question
Anyone see any problems with this approach?
Comment #28
mcdruidHmm thinking about it, maybe this is cheating as ConnectionUnitTest calls closeConnection() directly, and the whole point of #843114: DatabaseConnection::__construct() and DatabaseConnection_mysql::__construct() leaks $this (Too many connections) (which added that unit test) was to avoid e.g. test runs leaving connections open.
We could make deciding whether to call
closeConnection()
conditional on the PHP version (i.e. keep calling it for everything before PHP 8) to reduce the risks.Perhaps that's not a proper solution...
I think non-test use cases which involve cycling through lots of connections (examples of which are mentioned in that original issue) should still be able to close connections explicitly.
The problem just seems to be a handful of situations which come about in the test suite where db access is attempted (e.g on destruction of
DrupalCacheArray
) after the test runner swaps the connections over.Maybe the reasons those cases haven't caused problems before is because writing to cache / semaphore tables will succeed with or without the correct simpletest prefixes?
I'm inclined not to go too much further down the rabbit hole if we don't need to - so long as the test runners won't hit problems with too many connections building up...
Comment #29
mcdruidComment #30
mcdruidTo reduce risk by making the change even more minimal, we can omit the call to close the connection only for PHP 8 (and later).
Comment #31
Taran2Lhah, I was under impression that the actual working site is not affected, but test only ... great idea!
Just curious, is there a chance to fix the test runner here?
Comment #33
mcdruidGot the thumbs up from @Fabianx in chat.
@Taran2L what do you mean by "fix the test runner"? IIUC drupalci just uses the run-tests.sh script to run all tests in D7 core (I'm probably forgetting lots of other things it does too).
With any luck, this fix means that tests now run properly for PHP 8 on drupalci. We have about 13 failures left to fix.