summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Rothstein <drothstein@gmail.com>2012-12-08 18:31:33 -0500
committerDavid Rothstein <drothstein@gmail.com>2012-12-08 18:31:33 -0500
commitc7e45d937d6ac63b2f4fc5a44ce3567159f86052 (patch)
treee84dd975f5eb7449fd927bd3d81087aff6192d3b
parentef10c14b7f0ddf5fb949083a34ddef5ebd2928f6 (diff)
downloadbrdo-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.txt4
-rw-r--r--includes/database/database.inc41
-rw-r--r--includes/database/mysql/database.inc17
-rw-r--r--modules/simpletest/tests/database_test.test219
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);
+ }
+
+}