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

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Status: Active » Needs review
StatusFileSize
new4.67 KB

Note this functionality of closing connections is throughly tested in \Drupal\KernelTests\Core\Database\ConnectionUnitTest

alexpott’s picture

StatusFileSize
new750 bytes
new4.7 KB

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

The last submitted patch, 2: 3128616-2.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 3: 3128616-3.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new1.09 KB
new5.24 KB
new1.09 KB
new5.24 KB

Locally \Drupal\KernelTests\Core\Database\ConnectionUnitTest passes fine once you add the gc_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.

alexpott’s picture

Category: Task » Bug report
StatusFileSize
new1.99 KB
new7.23 KB

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

The last submitted patch, 7: 3128616-7.test-only.patch, failed testing. View results

daffie’s picture

daffie’s picture

StatusFileSize
new1.09 KB
new7.6 KB

Added something extra to the test to see if it works.

alexpott’s picture

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

daffie’s picture

@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 to gc_collect_cycles() in Drupal\Core\Database\Database::closeConnection() the test will fail.

Status: Needs review » Needs work

The last submitted patch, 10: 3128616-10.patch, failed testing. View results

alexpott’s picture

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

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new7.23 KB

What #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.

daffie’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Database/Database.php
    @@ -405,26 +405,15 @@ public static function closeConnection($target = NULL, $key = NULL) {
    +    gc_collect_cycles();
    

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

  2. +++ b/core/lib/Drupal/Core/Database/Database.php
    @@ -405,26 +405,15 @@ public static function closeConnection($target = NULL, $key = NULL) {
    +    // Force garbage collection of PDO connection objects so the connections
    +    // are really closed.
    

    Can we change this to: "Force the garbage collection to run. In that way PDO connection objects are really closed"

  3. In Drupal\Core\Database\Connection we also have the class variable $logger with a link to Drupal\Core\Database\Log. Should that also not be set to NULL in __destruct()?
  4. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -262,6 +265,24 @@ public function destroy() {
    +      $this->connection = NULL;
    

    Should $this->connection not always be set to NULL, instead of now only when the if-statement is true?

  5. Should we not add the code 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 of stdClass::__destruct() do?
  6. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -251,8 +251,11 @@ public static function open(array &$connection_options = []) {}
    +   * @deprecated @todo
    

    Fix the @todo.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new4.27 KB
new5.18 KB

@daffie

  1. Nope it won't. For what it is worth the \Drupal\Core\Database\Database::closeConnection() method is not actually called during regular Drupal operation. It is called during the install db tasks and that's probably the only time most sites will ever have it called.
  2. I've improved the wording. The connection objects are not closed. The connections are closed.
  3. It's not necessary. Schema doesn't actually need to be set to NULL anymore either.
  4. Sure - it doesn't matter but it results in less thinking so +1
  5. We don't call parent::__construct() in all our constructors. This is not necessary.
  6. Created https://www.drupal.org/node/3142866 and will fill it out once this has settled

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.

alexpott’s picture

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

daffie’s picture

Status: Needs review » Needs work

Will do a full review after the testbot returns green.

+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -319,6 +319,8 @@ public function testSchema($expected, $driver, $namespace) {
+   * @expectedDeprecation @todo

Nitpick: Fix the @todo.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new818 bytes
new5.37 KB

@daffie nice spot because of #19 it won't be green :)

The last submitted patch, 17: 3128616-17.patch, failed testing. View results

alexpott’s picture

StatusFileSize
new3.83 KB
new7.51 KB

And 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()

The last submitted patch, 20: 3128616-20.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 22: 3128616-22.patch, failed testing. View results

alexpott’s picture

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

mondrake’s picture

#25 a PDOStatement object left open after a PDO::prepare, somewhere, maybe?

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new1.77 KB
new8.62 KB

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

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

Lovely! Let's make objects do OOP work! LGTM

Nit:

+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -336,6 +339,29 @@ public function testDestroy() {
+    $mock_pdo = $this->createMock('Drupal\Tests\Core\Database\Stub\StubPDO');
+    $fake_connection = new class($mock_pdo, ['namespace' => 'Drupal\\Tests\\Core\\Database\\Stub\\Driver']) extends StubConnection {

maybe we can use StubPDO::class instead of FQCN here?

xjm’s picture

I 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 in core/lib/Drupal/Core/Lock/DatabaseLockBackend.php which 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.

xjm’s picture

xjm’s picture

From #1811730-14: Refactor Database to rely on the DIC solely:

Note that adding a __destruct() to Connection can not replace destroy(), the purpose of destroy() is to remove circular references so that the connection actually can be destructed, it needs to be called manually.

xjm’s picture

I also want a "Needs Sascha review" tag.

berdir’s picture

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

+++ b/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php
@@ -38,4 +40,37 @@ public function testDbNextId() {
+    // Test that \Drupal\Core\Database\Driver\mysql\Connection::__destruct()
+    // successfully trims the sequences table if the connection is closed.
+    $count = $this->connection->select('sequences')->countQuery()->execute()->fetchField();
+    $this->assertEquals(1, $count);
+  }

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?

ghost of drupal past’s picture

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

xjm’s picture

Status: Reviewed & tested by the community » Needs review

Better status.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Contrib database driver

I 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

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

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

mondrake’s picture

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

ghost of drupal past’s picture

Thanks, 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.

alexpott’s picture

@Charlie ChX Negyesi I've updated the CR title.

daffie’s picture

  1. +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
    @@ -336,6 +339,29 @@ public function testDestroy() {
    +    $fake_connection = NULL;
    

    The test fails when I change the line to: $fake_connection->__destruct();

    The error that I get is:

    Drupal\Tests\Core\Database\ConnectionTest::testDestructBcLayer
    Error: Call to a member function setAttribute() on null
    
    /var/www/drupalvm/drupal/core/lib/Drupal/Core/Database/Connection.php:275
    /var/www/drupalvm/drupal/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php:353
    /var/www/drupalvm/drupal/core/lib/Drupal/Core/Database/Connection.php:286
    /var/www/drupalvm/drupal/vendor/phpunit/phpunit/src/Framework/TestResult.php:691
    /var/www/drupalvm/drupal/vendor/phpunit/phpunit/src/Framework/TestSuite.php:597
    /var/www/drupalvm/drupal/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:621
    /var/www/drupalvm/drupal/vendor/phpunit/phpunit/src/TextUI/Command.php:204
    /var/www/drupalvm/drupal/vendor/phpunit/phpunit/src/TextUI/Command.php:163
    
  2. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -258,15 +258,36 @@ public static function open(array &$connection_options = []) {}
    +    if ($backtrace[1]['class'] != self::class && $backtrace[1]['function'] !== '__destruct') {
    

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

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new9.34 KB
new2.4 KB

Re #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.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

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

mondrake’s picture

Actually #42 doesn’t address #28, since another prexisting method is changed, not the one introduced by the patch itself. Superminor though...

daffie’s picture

StatusFileSize
new855 bytes
new9.31 KB

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

alexpott’s picture

StatusFileSize
new1.63 KB
new8.79 KB

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

mondrake’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll
daffie’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs reroll
StatusFileSize
new8.8 KB

Straight reroll.

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -258,15 +258,36 @@ public static function open(array &$connection_options = []) {}
+
+  /**
+   * Ensures that the PDO connection can be garbage collected.
+   */
+  public function __destruct() {
+    // Call the ::destroy method to provide a BC layer.
+    $this->destroy();
+    // Ensure that the circular reference caused by Connection::__construct()
+    // using $this in the call to set the statement class can be garbage
+    // collected.
+    $this->connection = NULL;
   }
 

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

alexpott’s picture

Good idea. The thing to do in 10.0.x is to remove the call to $this->destroy();

In some ways this is covered by

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -258,15 +258,36 @@ public static function open(array &$connection_options = []) {}
+   *
+   * @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Move custom
+   *   database destruction logic to __destruct().
+   *
+   * @see https://www.drupal.org/node/3142866
    */
   public function destroy() {

Because once we remove that implementation tests will start failing. But there's no harm in being explicit.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community
Related issues: +#3153864: Remove \Drupal\Core\Database\Connection::destroy() BC layer
StatusFileSize
new673 bytes
new8.9 KB

@todo and D10 issue added - #3153864: Remove \Drupal\Core\Database\Connection::destroy() BC layer

As this is only a comment set back to rtbc.

catch’s picture

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

alexpott’s picture

It doesn't need to be copied.

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -258,15 +258,38 @@ public static function open(array &$connection_options = []) {}
+      // Destroy all references to this connection by setting them to NULL.
+      // The Statement class attribute only accepts a new value that presents a
+      // proper callable, so we reset it to PDOStatement.
+      if (!empty($this->statementClass)) {
+        $this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, ['PDOStatement', []]);
+      }
+      $this->schema = NULL;

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.

catch’s picture

This is the kind of explanation I was hoping we could add to the comment.

alexpott’s picture

StatusFileSize
new801 bytes
new17.56 KB

Added this information to the @todo

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Update/UpdateCompilerPass.php
@@ -27,28 +27,16 @@ public function process(ContainerBuilder $container) {
-
-        // Ensure all the method call arguments are valid.
-        foreach ($definition->getMethodCalls() as $call) {
-          foreach ($call[1] as $argument) {
-            if ($this->isArgumentMissingService($argument, $container)) {
+          if ($argument instanceof Reference) {
+            $argument_id = (string) $argument;
+            if (!$container->has($argument_id) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {
+              // If the container does not have the argument and would throw an
+              // exception, remove the service.
               $container->removeDefinition($key);
-              $container->log($this, sprintf('Removed service "%s"; reason: method call "%s" depends on non-existent service "%s".', $key, $call[0], (string) $argument));
+              $container->log($this, sprintf('Removed service "%s"; reason: depends on non-existent service "%s".', $key, $argument_id));
               $has_changed = TRUE;
               $process_aliases = TRUE;
-              // Process the next definition.
-              continue 3;
             }
           }
         }

This is from a different patch.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new7.11 KB
new9 KB

Doh... didin't rebase properly. The interdiff shows everything that's not supposed to be in the patch being reverted...

Rebased my locally branch properly.

  • catch committed 2ee59be on 9.1.x
    Issue #3128616 by alexpott, daffie, mondrake, xjm, Charlie ChX Negyesi:...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed 2ee59be and pushed to 9.1.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

quietone’s picture

Publish CR