diff options
author | David Rothstein <drothstein@gmail.com> | 2012-12-08 18:31:33 -0500 |
---|---|---|
committer | David Rothstein <drothstein@gmail.com> | 2012-12-08 18:31:33 -0500 |
commit | c7e45d937d6ac63b2f4fc5a44ce3567159f86052 (patch) | |
tree | e84dd975f5eb7449fd927bd3d81087aff6192d3b | |
parent | ef10c14b7f0ddf5fb949083a34ddef5ebd2928f6 (diff) | |
download | brdo-c7e45d937d6ac63b2f4fc5a44ce3567159f86052.tar.gz brdo-c7e45d937d6ac63b2f4fc5a44ce3567159f86052.tar.bz2 |
Issue #843114 by sun, quicksketch, Berdir | c960657: Fixed DatabaseConnection::__construct() and DatabaseConnection_mysql()::__construct() leaks $this (Too many connections).
-rw-r--r-- | CHANGELOG.txt | 4 | ||||
-rw-r--r-- | includes/database/database.inc | 41 | ||||
-rw-r--r-- | includes/database/mysql/database.inc | 17 | ||||
-rw-r--r-- | modules/simpletest/tests/database_test.test | 219 |
4 files changed, 266 insertions, 15 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d4ffec79e..1285a917f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,9 +1,11 @@ Drupal 7.18, xxxx-xx-xx (development version) ----------------------- +- Fixed a bug which caused the database API to not properly close database + connections. - Added link to the URL for running cron from outside the site to the Cron settings page (UI change). -- Fixed bug which prevented image styles from being reverted on PHP 5.4. +- Fixed a bug which prevented image styles from being reverted on PHP 5.4. - Made the default .htaccess rules protocol sensitive to improve security for sites which use HTTPS and redirect between "www" and non-"www" versions of the page. diff --git a/includes/database/database.inc b/includes/database/database.inc index cae50fb87..26ce6fcf4 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -194,7 +194,7 @@ abstract class DatabaseConnection extends PDO { /** * The key representing this connection. - * + * * The key is a unique string which identifies a database connection. A * connection can be a single server or a cluster of master and slaves (use * target to pick between master and slave). @@ -303,13 +303,29 @@ abstract class DatabaseConnection extends PDO { // Call PDO::__construct and PDO::setAttribute. parent::__construct($dsn, $username, $password, $driver_options); - // Set a specific PDOStatement class if the driver requires that. + // Set a Statement class, unless the driver opted out. if (!empty($this->statementClass)) { $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); } } /** + * Destroys this Connection object. + * + * PHP does not destruct an object if it is still referenced in other + * variables. In case of PDO database connection objects, PHP only closes the + * connection when the PDO object is destructed, so any references to this + * object may cause the number of maximum allowed connections to be exceeded. + */ + public function destroy() { + // 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. + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); + $this->schema = NULL; + } + + /** * Returns the default query options for any given query. * * A given query can be customized with a number of option flags in an @@ -1627,8 +1643,8 @@ abstract class Database { */ final public static function removeConnection($key) { if (isset(self::$databaseInfo[$key])) { + self::closeConnection(NULL, $key); unset(self::$databaseInfo[$key]); - unset(self::$connections[$key]); return TRUE; } else { @@ -1694,11 +1710,24 @@ abstract class Database { if (!isset($key)) { $key = self::$activeKey; } - // To close the connection, we need to unset the static variable. + // To close a connection, it needs to be set to NULL and removed from the + // static variable. In all cases, closeConnection() might be called for a + // connection that was not opened yet, in which case the key is not defined + // yet and we just ensure that the connection key is undefined. if (isset($target)) { + if (isset(self::$connections[$key][$target])) { + self::$connections[$key][$target]->destroy(); + self::$connections[$key][$target] = NULL; + } unset(self::$connections[$key][$target]); } else { + if (isset(self::$connections[$key])) { + foreach (self::$connections[$key] as $target => $connection) { + self::$connections[$key][$target]->destroy(); + self::$connections[$key][$target] = NULL; + } + } unset(self::$connections[$key]); } } @@ -1852,8 +1881,8 @@ class DatabaseTransaction { */ protected $name; - public function __construct(DatabaseConnection &$connection, $name = NULL) { - $this->connection = &$connection; + public function __construct(DatabaseConnection $connection, $name = NULL) { + $this->connection = $connection; // If there is no transaction depth, then no transaction has started. Name // the transaction 'drupal_transaction'. if (!$depth = $connection->transactionDepth()) { diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc index 7ad019e58..00d81f473 100644 --- a/includes/database/mysql/database.inc +++ b/includes/database/mysql/database.inc @@ -13,11 +13,11 @@ class DatabaseConnection_mysql extends DatabaseConnection { /** - * Flag to indicate if we have registered the nextID cleanup function. + * Flag to indicate if the cleanup function in __destruct() should run. * * @var boolean */ - protected $shutdownRegistered = FALSE; + protected $needsCleanup = FALSE; public function __construct(array $connection_options = array()) { // This driver defaults to transaction support, except if explicitly passed FALSE. @@ -78,6 +78,12 @@ class DatabaseConnection_mysql extends DatabaseConnection { $this->exec(implode('; ', $connection_options['init_commands'])); } + public function __destruct() { + if ($this->needsCleanup) { + $this->nextIdDelete(); + } + } + public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); } @@ -115,12 +121,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id)); $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID)); } - if (!$this->shutdownRegistered) { - // Use register_shutdown_function() here to keep the database system - // independent of Drupal. - register_shutdown_function(array($this, 'nextIdDelete')); - $shutdownRegistered = TRUE; - } + $this->needsCleanup = TRUE; return $new_id; } diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 6e1d15979..2c096fb1d 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -3815,3 +3815,222 @@ class DatabaseEmptyStatementTestCase extends DrupalWebTestCase { $this->assertEqual($result->fetchAll(), array(), t('Empty array returned from empty result set.')); } } + +/** + * Tests management of database connections. + */ +class ConnectionUnitTest extends DrupalUnitTestCase { + + protected $key; + protected $target; + + protected $monitor; + protected $originalCount; + + public static function getInfo() { + return array( + 'name' => 'Connection unit tests', + 'description' => 'Tests management of database connections.', + 'group' => 'Database', + ); + } + + function setUp() { + parent::setUp(); + + $this->key = 'default'; + $this->originalTarget = 'default'; + $this->target = 'DatabaseConnectionUnitTest'; + + // Determine whether the database driver is MySQL. If it is not, the test + // methods will not be executed. + // @todo Make this test driver-agnostic, or find a proper way to skip it. + // @see http://drupal.org/node/1273478 + $connection_info = Database::getConnectionInfo('default'); + $this->skipTest = (bool) $connection_info['default']['driver'] != 'mysql'; + if ($this->skipTest) { + // Insert an assertion to prevent Simpletest from interpreting the test + // as failure. + $this->pass('This test is only compatible with MySQL.'); + } + + // Create an additional connection to monitor the connections being opened + // and closed in this test. + // @see TestBase::changeDatabasePrefix() + $connection_info = Database::getConnectionInfo('default'); + Database::addConnectionInfo('default', 'monitor', $connection_info['default']); + global $databases; + $databases['default']['monitor'] = $connection_info['default']; + $this->monitor = Database::getConnection('monitor'); + } + + /** + * Adds a new database connection info to Database. + */ + protected function addConnection() { + // Add a new target to the connection, by cloning the current connection. + $connection_info = Database::getConnectionInfo($this->key); + Database::addConnectionInfo($this->key, $this->target, $connection_info[$this->originalTarget]); + + // Verify that the new target exists. + $info = Database::getConnectionInfo($this->key); + // Note: Custom assertion message to not expose database credentials. + $this->assertIdentical($info[$this->target], $connection_info[$this->key], 'New connection info found.'); + } + + /** + * Returns the connection ID of the current test connection. + * + * @return integer + */ + protected function getConnectionID() { + return (int) Database::getConnection($this->target, $this->key)->query('SELECT CONNECTION_ID()')->fetchField(); + } + + /** + * Asserts that a connection ID exists. + * + * @param integer $id + * The connection ID to verify. + */ + protected function assertConnection($id) { + $list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0); + return $this->assertTrue(isset($list[$id]), format_string('Connection ID @id found.', array('@id' => $id))); + } + + /** + * Asserts that a connection ID does not exist. + * + * @param integer $id + * The connection ID to verify. + */ + protected function assertNoConnection($id) { + $list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0); + return $this->assertFalse(isset($list[$id]), format_string('Connection ID @id not found.', array('@id' => $id))); + } + + /** + * Tests Database::closeConnection() without query. + * + * @todo getConnectionID() executes a query. + */ + function testOpenClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a query. + */ + function testOpenQueryClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Execute a query. + Database::getConnection($this->target, $this->key)->query('SHOW TABLES'); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a query and custom prefetch method. + */ + function testOpenQueryPrefetchClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Execute a query. + Database::getConnection($this->target, $this->key)->query('SHOW TABLES')->fetchCol(); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a select query. + */ + function testOpenSelectQueryClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Create a table. + $name = 'foo'; + Database::getConnection($this->target, $this->key)->schema()->createTable($name, array( + 'fields' => array( + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + )); + + // Execute a query. + Database::getConnection($this->target, $this->key)->select('foo', 'f') + ->fields('f', array('name')) + ->execute() + ->fetchAll(); + + // Drop the table. + Database::getConnection($this->target, $this->key)->schema()->dropTable($name); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + +} |