Postgres driver issue with nested savepoints mimic_implicit_commit duplicated

Created on 2 November 2017, about 7 years ago
Updated 22 February 2024, 9 months ago

We're having an issue with Workbench Moderation and Group installed and have traced it back to an issue with the Postgres db driver. The Postgres driver automatically creates savepoints named "mimic_implicit_commit" for most queries running inside of an ongoing transaction.

The problem occurs when a DB query is run that triggers another DB query in the first's preExecute() phase (typically a hook such as hook_node_grants). This creates a situation where a new savepoint also called "mimic_implicit_commit" is added before the original "mimic_implicit_commit" savepoint is released. pushTransaction() doesn't allow for duplicate savepoint names.

The easiest way to re-create is to install/enable Workbench Moderation and then have any module that implements hook_node_grants() with a DB query using the ->execute() method enabled as well (such as Group/Group Node). When you add try to add content as a user without "bypass node access", you'll get this error message when you try to save:

Drupal\Core\Entity\EntityStorageException: mimic_implicit_commit is already in use. in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 777 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

Here's some additional information about the problem. Basically all DB queries funnel into the pgsql\Select class's execute() method, which arbitrarily runs addSavepoint($savepoint_name = 'mimic_implicit_commit')

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%...

# Drupal\Core\Database\Driver\pgsql\Select
public function execute() {
  $this->connection->addSavepoint();
  try {
    $result = parent::execute();
  }
  catch (\Exception $e) {
    $this->connection->rollbackSavepoint();
    throw $e;
  }
  $this->connection->releaseSavepoint();

  return $result;
}

pgsql\Select\execute() is called from many different places. Only one path via the pgsql/Connection query() method does it actually check for the duplicate 'mimic_implicit_commit' name:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%...

# Drupal\Core\Database\Driver\pgsql\Connection
public function query($query, array $args = [], $options = []) {
  $options += $this->defaultOptions();

  // The PDO PostgreSQL driver has a bug which doesn't type cast booleans
  // correctly when parameters are bound using associative arrays.
  // @see http://bugs.php.net/bug.php?id=48383
  foreach ($args as &$value) {
    if (is_bool($value)) {
      $value = (int) $value;
    }
  }

  // We need to wrap queries with a savepoint if:
  // - Currently in a transaction.
  // - A 'mimic_implicit_commit' does not exist already.
  // - The query is not a savepoint query.
  $wrap_with_savepoint = $this->inTransaction() &&
    !isset($this->transactionLayers['mimic_implicit_commit']) &&
    !(is_string($query) && (
    stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
    stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
    stripos($query, 'SAVEPOINT ') === 0
    )
    );
  if ($wrap_with_savepoint) {
    // Create a savepoint so we can rollback a failed query. This is so we can
    // mimic MySQL and SQLite transactions which don't fail if a single query
    // fails. This is important for tables that are created on demand. For
    // example, \Drupal\Core\Cache\DatabaseBackend.
    $this->addSavepoint();
    try {
      $return = parent::query($query, $args, $options);
      $this->releaseSavepoint();
    }
    catch (\Exception $e) {
      $this->rollbackSavepoint();
      throw $e;
    }
  }
  else {
    $return = parent::query($query, $args, $options);
  }

  return $return;
}

However, both Groups and Workbench moderation call the pgsql/Select execute() method directly, bypassing the duplicate check for mimic_implicit_commit in pgsql/Connection query().

Groups:

#0  Drupal\Core\Database\Driver\pgsql\Select->execute() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/DatabaseStorage.php:273]
#1  Drupal\Core\Config\DatabaseStorage->listAll() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/CachedStorage.php:207]
#2  Drupal\Core\Config\CachedStorage->findByPrefix() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/CachedStorage.php:183]
#3  Drupal\Core\Config\CachedStorage->listAll() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/ConfigFactory.php:328]
#4  Drupal\Core\Config\ConfigFactory->listAll() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/Entity/Query/Query.php:172]
#5  Drupal\Core\Config\Entity\Query\Query->loadRecords() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Config/Entity/Query/Query.php:82]
#6  Drupal\Core\Config\Entity\Query\Query->execute() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Entity/EntityStorageBase.php:503]
#7  Drupal\Core\Entity\EntityStorageBase->loadByProperties() called at [/opt/tbs/wcms/open_gov/web/current/html/modules/contrib/group/src/GroupMembershipLoader.php:146]
#8  Drupal\group\GroupMembershipLoader->loadByUser() called at [/opt/tbs/wcms/open_gov/web/current/html/modules/contrib/group/modules/gnode/gnode.module:169]

Workbench:

#0  Drupal\Core\Database\Driver\pgsql\Select->execute() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Entity/Query/Sql/Query.php:250]
#1  Drupal\Core\Entity\Query\Sql\Query->result() called at [/opt/tbs/wcms/open_gov/web/current/html/core/lib/Drupal/Core/Entity/Query/Sql/Query.php:77]
#2  Drupal\Core\Entity\Query\Sql\Query->execute() called at [/opt/tbs/wcms/open_gov/web/current/html/modules/contrib/workbench_moderation/src/ModerationInformation.php:166]
#3  Drupal\workbench_moderation\ModerationInformation->getLatestRevisionId() called at [/opt/tbs/wcms/open_gov/web/current/html/modules/contrib/workbench_moderation/src/ModerationInformation.php:151]

The pgsql driver may need to be patched to handle the duplicate savepoint condition when Query class's execute() function are accessed directly.

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
PostgreSQL driver 

Last updated about 2 months ago

No maintainer
Created by

🇨🇦Canada jeffdavidgordon

Live updates comments and jobs are added and updated live.
  • PostgreSQL

    Particularly affects sites running on the PostgreSQL database.

  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

  • Needs issue summary update

    Issue summaries save everyone time if they are kept up-to-date. See Update issue summary task instructions.

  • Needs manual testing

    The change/bugfix cannot be fully demonstrated by automated testing, and thus requires manual testing in a variety of environments.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.71.5 2024