Problem/Motivation

In #2664322: key_value table is only used by a core service but it depends on system install there were test failures with PostgreSQL in ConnectionFailureTest that seemed to be caused by the Connection class having an incorrect statementClass. This was created as a follow-up as it should not block that issue.

Proposed resolution

The issue seems to have come from an inconsistency between the mysql driver and the pgsql driver in the tableExists method. The pg_tables view may be too slow. Find an alternative that is faster. See comment #17. Testing is covered under the ConnectionFailureTest class per @daffie.

A follow-up issue was created to address similar issues with findTables in #2921855: Default Database Schema::findTables() is slow for PostgreSQL.

Remaining tasks

-

User interface changes

None

API changes

None

Data model changes

None

Comments

benjifisher created an issue. See original summary.

benjifisher’s picture

Status: Active » Needs review
StatusFileSize
new1.08 KB

See comments #74 through #99 (and perhaps more recent) for the initial discussion of this issue. Credit should go to @mradcliffe and @almaudoh.

I am attaching a patch based on 2664322-94.patch, taking only the part that applies to this issue. I will be surprised if any tests fail on this patch.

We still need to create a test that is fixed by this patch.

benjifisher’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests
mradcliffe’s picture

Status: Needs work » Needs review
StatusFileSize
new1.58 KB

I'm wondering if we could make the $connection variable an explicit reference of a private variable that holds the PDO connection object?

Theoretical patch attached. Probably breaks horribly.

mradcliffe’s picture

Status: Needs review » Needs work

Flip back to needs work.

benjifisher’s picture

@mradcliffe:

I am glad to see you working on this issue~ I think you have a better idea than I what is going on here.

Can you explain what you meant in #2664322-78: key_value table is only used by a core service but it depends on system install when you said,

All three queries being run, the watchdog insert query, the key_value store select query, and the table exists query are being run with PDOStatement.

The patch being tested makes the key_value table lazy-loaded, so that seems like a good place to look, but I do not know where you see a query to that table. I think you are talking about this test (long comment snipped):

  function testConnectionFailureLogging() {
    $logger = \Drupal::service('logger.factory');
    Database::closeConnection();

    // Create a log entry.
    $logger->get('php')->error('testConnectionFailureLogging');

    // Re-establish the default database connection.
    Database::getConnection();

    $wid = db_query("SELECT MAX(wid) FROM {watchdog} WHERE message = 'testConnectionFailureLogging'")->fetchField();
    $this->assertTrue($wid, 'Watchdog entry has been stored in database.');
  }

Should I dig into the first line of the test to see what you mean?

mradcliffe’s picture

Status: Needs work » Needs review
StatusFileSize
new4.05 KB
new2.47 KB

@benjifisher:

Although I can't recall the specific thing I did to notice that, what I had been doing was to add some debug code to check what class/interface the connection class's statement was using. I think I needed to add the debug code because I was running the one test directly on the command line through run-tests script. Perhaps I put that code in Connection::destroy right before that condition, but I'm not sure exactly why I said that about key_value table query. I

I'm actually surprised the patch I posted in #4 was as successful as it was in the Postgresql run.

Let's move my logic into the pgsql driver instead and then check NULL in Database::openConnection for whether a new connection should be opened as well as isset condition.

mradcliffe’s picture

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -157,6 +174,21 @@ public static function open(array &$connection_options = array()) {
+      $this->actualConnection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));

Maybe also setting $this->connection = &$this->actualConnection; would be safer as well even if there are no explicit test failures.

Status: Needs review » Needs work

The last submitted patch, 7: 2847855-7.patch, failed testing.

benjifisher’s picture

Status: Needs work » Needs review
StatusFileSize
new1.42 KB

I have been doing some old-fashioned debugging, trying to come up with a minimal version of the patch from #2664322: key_value table is only used by a core service but it depends on system install that still produces the test failure. The attached patch is what I have come up with. Locally, it passes with MySQL and fails with PostgresQL. I hope the testbot confirms this!

One odd thing is that, when I run Xdebug, I never get to the line of code that throws an uncaught exception. Is that why you suspect a race condition, @mradcliffe?

The problem seems to come from Drupal\Core\KeyValueStore\DatabaseStorage::getMultiple().

Status: Needs review » Needs work

The last submitted patch, 10: 2847855-fail-10.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review
StatusFileSize
new2.31 KB
new906 bytes

Maybe the problem is not the PostgreSQL database, but the different implementations of the method tableExists. This patch make the implementation of PostgreSQL version the same as that of the MySQL version. Lets see if that makes any difference.

Status: Needs review » Needs work

The last submitted patch, 12: 2847855-fail-12.patch, failed testing.

cilefen’s picture

The priority may be higher: https://www.drupal.org/core/issue-priority

benjifisher’s picture

@cilefen: I am not sure why you think the priority may be higher than "normal". Are you thinking of the criterion "Cause test failures in environments not supported by the automated testing platform" for "major" priority? The test failure is supported by the testing platform, although the test for PostreSQL is not run on all patches.

@daffie: I think we are getting close! If I did not need to take a break for sleep, I would have dug into tableExists() myself.

For the sake of readers who have not studied the test failures: the "fail" patch in #10 introduces a bunch of new failures on both MySQL and PostgreSQL, and it succeeds in triggering the failure in ConnectionFailureTest() only with PostgreSQL. I consider this "fail" patch a success. ;) The "fail" patch in #12 only triggers the new failures, giving the same behavior for MySQL and PostgreSQL.

There are a bunch of things going on here, and maybe they should be handled in separate issues. (I have not checked yet: some of those issues may already exist.)

  1. As @mradcliffe pointed out, there seems to be a race condition when the DB connection is closed and then reopened, leading to some object having the class PDOStatement instead of Statement. This causes an exception in Drupal\Core\KeyValueStore\DatabaseStorage::getMultiple().
  2. The exception handling in Drupal\Core\KeyValueStore\DatabaseStorage::getMultiple() is very lax.
  3. Different DB drivers (MySQL, PostgreSQL, ... what about SQLite?) implement tableExists() differently.
cilefen’s picture

Seriously I'm just on a phone and saw "race condition".

mradcliffe’s picture

Issue summary: View changes

Oh, wow, nice find @daffie. I didn't know there was a difference.

Updated issue summary.

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
@@ -466,9 +466,13 @@ protected function createPrimaryKeySql($fields) {
+      $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);

This needs to be wrapped in a transaction (probably using the connection::addSavepoint() method iirc) so that a failure does not stop any subsequent writes.

I think a somewhat faster query than pg_tables could be to join pg_catalog.pg_namespace and pg_catalog.pg_class. But this may not be as fast as a transaction-wrapped select on the table itself.

SELECT 1 FROM pg_catalog.pg_class c INNER JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) WHERE n.nspname='public' AND c.relkind = 'r' AND relname = :tablename

daffie’s picture

Status: Needs work » Needs review
StatusFileSize
new3.82 KB
new44.7 KB

There are 4 different versions of the method Schema::tableExists(). One version for all databases and three other that all override the base version. This patch adds the code for each version in a try-catch statement. I have added two patch: one with the fix only and one with the fix and the patch from #2664322-111: key_value table is only used by a core service but it depends on system install. Both patches should pass the testbot.

@mradcliffe: Making the code faster is a great idea, but lets do that in another issue.

Mixologic’s picture

I just spent the entire day yesterday digging into the performance issues on postgres on the testbots, because we dont really want postgres to be that much slower in testing, and therefore cost us more resources.

So, what I discovered is that huge majority of the time spend in queries in postgres are to the information_schema tables. In #postgresql yesterday I was made aware that the queries to the information_schema table are *very* slow, :

[02/02/2017 -:- 02:45:23 PM]  <RhodiumToad>	ok. the information_schema views are crippled by the SQL spec and generate _really bad_ query plans
[02/02/2017 -:- 02:45:54 PM]  <RhodiumToad>	so, if you need to test whether tables exist or whatever, and you care about performance, you use the system catalogs instead

Which tells me that we should avoid hitting information_schema at all. pg_class and pg_type are the two system catalogs that end up getting hammered with disk writes.

During a mysql test, there is 7.8 GB of total disk writes, and during a pgsql test, there is about 180 GB of disk writing, and almost all of it is to the two files that comrprise pg_class and pg_type.

daffie’s picture

Issue tags: -Needs tests
StatusFileSize
new44.93 KB
new4.04 KB
new1.1 KB

Changed the PostgreSQL query for Schema::tableExists() so that it will be faster. Using the query from @mradcliffe in comment #17.

Removing the "Needs tests" tag, because the test ConnectionFailureTest does the testing we need for this problem.

daffie’s picture

StatusFileSize
new44.75 KB
new3.86 KB
new4.28 KB

Forgot to add the transaction savepoints.

The last submitted patch, 21: 2847855-21-with-patch-2664322-111.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 21: 2847855-21.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review
StatusFileSize
new1.54 KB
new4.12 KB
new45 KB

Only adding transaction savepoints to the PostgreSQL method Schema::tableExists().

benjifisher’s picture

@daffie, it looks as though you already suggested simplifying the implementations of tableExists() in #2797141: Remove the methods tableExists() and fieldExists() from Drupal\Core\Database\Driver\mysql\Schema. I am adding that as a related issue.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mradcliffe’s picture

Issue summary: View changes
Issue tags: +DrupalEurope, +PostgreSQL

I updated the issue summary based on comments and added the tests for a spin on 8.7.x. I'm sorry for taking so long.

I looked at the latest patch in #24 and that makes sense. I think we're covering all the bases here by wrapping in our savepoint transaction.

mradcliffe’s picture

Status: Needs review » Needs work

Needs a re-roll.

nigelcunningham’s picture

Assigned: Unassigned » nigelcunningham

I'm intending to pick this up tomorrow as it and https://www.drupal.org/project/drupal/issues/2664322 are causing me issues with a custom profile install. Assigning to self for that reason.

nigelcunningham’s picture

Updating to 8.7-dev at the time of writing (88ad0137b5d0cd9267160e29).

nigelcunningham’s picture

StatusFileSize
new47.63 KB

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

daffie’s picture

Version: 8.9.x-dev » 9.0.x-dev
Assigned: nigelcunningham » Unassigned
Status: Needs work » Needs review
StatusFileSize
new8.86 KB
new4.79 KB

Rerolled the patch and added testing.

Status: Needs review » Needs work

The last submitted patch, 36: 2847855-36-tests-only-should-fail.patch, failed testing. View results

benjifisher’s picture

Status: Needs work » Needs review

The failures are expected, so I am setting the status back to NR.

Order matters. When uploading multiple patches, one of which is expected to fail, add a patch that is expected to pass last.

andypost’s picture

Just quickly skimmed the patch and it looks sqlite could be affected by related

+++ b/core/tests/Drupal/Tests/Core/Database/SchemaTest.php
@@ -0,0 +1,120 @@
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
...
+    $condition = $this->getMockBuilder('Drupal\Core\Database\Query\Condition')
...
+    $schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
...
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
...
+    $schema = $this->getMockBuilder('Drupal\Core\Database\Driver\pgsql\Schema')
...
+    $connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
...
+    $schema = $this->getMockBuilder('Drupal\Core\Database\Driver\sqlite\Schema')

Better to use ::class notation, it is shorter and better navigable in IDEs

daffie’s picture

StatusFileSize
new4.9 KB
new8.96 KB
new2.93 KB

Order matters. When uploading multiple patches, one of which is expected to fail, add a patch that is expected to pass last.

Did not know that, thought the ordering was random. @benjifisher: Thanks, I learned something new.

Addressed the remark made by @andypost in comment #39.

The last submitted patch, 40: 2847855-40-tests-only-should-fail.patch, failed testing. View results

mradcliffe’s picture

  1. +++ b/core/tests/Drupal/Tests/Core/Database/SchemaTest.php
    @@ -0,0 +1,125 @@
    +/**
    + * @group Database
    + */
    

    Shouldn't class doc block have a full sentence before @group?

  2. +++ b/core/tests/Drupal/Tests/Core/Database/SchemaTest.php
    @@ -0,0 +1,125 @@
    +  /**
    +   * @covers \Drupal\Core\Database\Schema::tableExists
    ...
    +  /**
    +   * @covers \Drupal\Core\Database\Driver\pgsql\Schema::tableExists
    ...
    +  /**
    +   * @covers \Drupal\Core\Database\Driver\sqlite\Schema::tableExists
    

    Shouldn't there be a full sentence before @covers or any other annotation?

  3. +++ b/core/tests/Drupal/Tests/Core/Database/SchemaTest.php
    @@ -0,0 +1,125 @@
    +    $this->assertFalse($schema->tableExists('table_does_not_extst'));
    ...
    +      ->will($this->returnValue(['schema' => 'main', 'table' => 'table_does_not_extst']));
    ...
    +    $this->assertFalse($schema->tableExists('table_does_not_extst'));
    ...
    +      ->will($this->returnValue(['schema' => 'main', 'table' => 'table_does_not_extst']));
    ...
    +    $this->assertFalse($schema->tableExists('table_does_not_extst'));
    

    I was a little confused by the "table_does_not_extst" as my mind wants to look at it as a typo "table_does_not_exist". It doesn't really matter, but it might be confusing to someone else.

daffie’s picture

StatusFileSize
new4.93 KB
new8.99 KB
new1.78 KB

Thank you for your review @mradcliffe.

1. Fixed.
2. I am not sure if there needs to be a full sentence before @covers. I followed the example in https://www.drupal.org/node/2116043#s-examples--3. The example is about @coversDefaultClass and applied the same idea to @covers. If you think that adding a sentence is an improvement then I will add one.
3. Fixed. Had to read your comment twice, before I saw my own typo.

The last submitted patch, 43: 2847855-43-tests-only-should-fail.patch, failed testing. View results

mradcliffe’s picture

Status: Needs review » Reviewed & tested by the community

Works for me.

benjifisher’s picture

StatusFileSize
new8.99 KB

Once an issue is RTBC, the latest patch is scheduled for daily re-testing. There may be exceptions for patches that were not scheduled for test when uploaded, or have the magic string "-do-not-test"; I am not sure. The string "-tests-only-should-fail" is not magic, and I see "re-tests daily" next to the test that we expect to fail.

See also the discussion in #38 and #40.

I am re-uploading the "right" patch from #43.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
@@ -489,7 +489,19 @@ protected function createPrimaryKeySql($fields) {
+    catch (\Exception $e) {

+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
@@ -27,10 +27,15 @@ class Schema extends DatabaseSchema {
+    catch (\Exception $e) {

+++ b/core/lib/Drupal/Core/Database/Schema.php
@@ -167,14 +167,19 @@ protected function buildTableNameCondition($table_name, $operator = '=', $add_pr
+    catch (\Exception $e) {

Is there a more specific exception to catch here - ie should we only catching \PDOException exceptions? Catching \Exception is very generic.

daffie’s picture

StatusFileSize
new9.01 KB
new3.29 KB

I see no problem in changing the use of \Exception to \PDOException.

xjm’s picture

Version: 9.0.x-dev » 8.8.x-dev

Since we should fix this bug in D8 too, I'm filing it against 8.8.x (which is the current bugfix support branch). Patches can be tested against other branches as needed when they're uploaded.

The issue will be automatically updated to 8.9.x after the last 8.8.x bugfix release. Thanks!

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new146 bytes

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.