1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-19 05:12:41 +01:00

Simplify Aphront transaction code

Summary:
In D1515, I introduced some excessively-complicated semantics for detecting
connections that are lost while transactional. These semantics cause us to
reenter establishConnection() and establish twice as many connections as we need
in the common case.

We don't need a hook there at all -- it's sufficient to throw the exception
rather than retrying the query when we encounter it. This doesn't have
reentrancy problems.

Test Plan:
  - Added some encapsulation-violating hooks and a unit test for them
  - Verified we no longer double-connect.

Reviewers: btrahan, nh

Reviewed By: btrahan

CC: aran, epriestley

Maniphest Tasks: T835

Differential Revision: https://secure.phabricator.com/D1576
This commit is contained in:
epriestley 2012-02-07 14:58:37 -08:00
parent f19d5fbeae
commit 4ac29d108c
5 changed files with 118 additions and 26 deletions

View file

@ -59,6 +59,7 @@ phutil_register_library_map(array(
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts', 'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts',
'AphrontListFilterView' => 'view/layout/listfilter', 'AphrontListFilterView' => 'view/layout/listfilter',
'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql', 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
'AphrontMySQLDatabaseConnectionTestCase' => 'storage/connection/mysql/__tests__',
'AphrontNullView' => 'view/null', 'AphrontNullView' => 'view/null',
'AphrontPHPHTTPSink' => 'aphront/sink/php', 'AphrontPHPHTTPSink' => 'aphront/sink/php',
'AphrontPageView' => 'view/page/base', 'AphrontPageView' => 'view/page/base',
@ -883,6 +884,7 @@ phutil_register_library_map(array(
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView', 'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
'AphrontListFilterView' => 'AphrontView', 'AphrontListFilterView' => 'AphrontView',
'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontNullView' => 'AphrontView', 'AphrontNullView' => 'AphrontView',
'AphrontPHPHTTPSink' => 'AphrontHTTPSink', 'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
'AphrontPageView' => 'AphrontView', 'AphrontPageView' => 'AphrontView',

View file

@ -23,7 +23,6 @@
abstract class AphrontDatabaseConnection { abstract class AphrontDatabaseConnection {
private static $transactionStates = array(); private static $transactionStates = array();
private $initializingTransactionState;
abstract public function getInsertID(); abstract public function getInsertID();
abstract public function getAffectedRows(); abstract public function getAffectedRows();
@ -121,9 +120,6 @@ abstract class AphrontDatabaseConnection {
* @task xaction * @task xaction
*/ */
public function isInsideTransaction() { public function isInsideTransaction() {
if ($this->initializingTransactionState) {
return false;
}
$state = $this->getTransactionState(); $state = $this->getTransactionState();
return ($state->getDepth() > 0); return ($state->getDepth() > 0);
} }
@ -137,17 +133,10 @@ abstract class AphrontDatabaseConnection {
* @task xaction * @task xaction
*/ */
protected function getTransactionState() { protected function getTransactionState() {
// Establishing a connection may be required to get the transaction key,
// and may also perform a test for transaction state. While establishing
// transaction state, avoid infinite recursion.
$this->initializingTransactionState = true;
$key = $this->getTransactionKey(); $key = $this->getTransactionKey();
if (empty(self::$transactionStates[$key])) { if (empty(self::$transactionStates[$key])) {
self::$transactionStates[$key] = new AphrontDatabaseTransactionState(); self::$transactionStates[$key] = new AphrontDatabaseTransactionState();
} }
$this->initializingTransactionState = false;
return self::$transactionStates[$key]; return self::$transactionStates[$key];
} }

View file

@ -24,6 +24,8 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
private $config; private $config;
private $connection; private $connection;
private $nextError;
private static $connectionCache = array(); private static $connectionCache = array();
public function __construct(array $configuration) { public function __construct(array $configuration) {
@ -113,11 +115,6 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
return; return;
} }
if ($this->isInsideTransaction()) {
throw new Exception(
"Connection was lost inside a transaction! Recovery is impossible.");
}
$start = microtime(true); $start = microtime(true);
if (!function_exists('mysql_connect')) { if (!function_exists('mysql_connect')) {
@ -245,6 +242,10 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
$profiler->endServiceCall($call_id, array()); $profiler->endServiceCall($call_id, array());
if ($this->nextError) {
$result = null;
}
if ($result) { if ($result) {
$this->lastResult = $result; $this->lastResult = $result;
break; break;
@ -252,23 +253,45 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
$this->throwQueryException($this->connection); $this->throwQueryException($this->connection);
} catch (AphrontQueryConnectionLostException $ex) { } catch (AphrontQueryConnectionLostException $ex) {
if ($this->isInsideTransaction()) {
// Zero out the transaction state to prevent a second exception
// ("program exited with open transaction") from being thrown, since
// we're about to throw a more relevant/useful one instead.
$state = $this->getTransactionState();
while ($state->getDepth()) {
$state->decreaseDepth();
}
// We can't close the connection before this because
// isInsideTransaction() and getTransactionState() depend on the
// connection.
$this->closeConnection();
throw $ex;
}
$this->closeConnection();
if (!$retries) { if (!$retries) {
throw $ex; throw $ex;
} }
if ($this->isInsideTransaction()) {
throw $ex;
}
$class = get_class($ex); $class = get_class($ex);
$message = $ex->getMessage(); $message = $ex->getMessage();
phlog("Retrying ({$retries}) after {$class}: {$message}"); phlog("Retrying ({$retries}) after {$class}: {$message}");
$this->closeConnection();
} }
} }
} }
private function throwQueryException($connection) { private function throwQueryException($connection) {
if ($this->nextError) {
$errno = $this->nextError;
$error = 'Simulated error.';
$this->nextError = null;
} else {
$errno = mysql_errno($connection); $errno = mysql_errno($connection);
$error = mysql_error($connection); $error = mysql_error($connection);
}
switch ($errno) { switch ($errno) {
case 2013: // Connection Dropped case 2013: // Connection Dropped
@ -294,4 +317,13 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
} }
} }
/**
* Force the next query to fail with a simulated error. This should be used
* ONLY for unit tests.
*/
public function simulateErrorOnNextQuery($error) {
$this->nextError = $error;
return $this;
}
} }

View file

@ -0,0 +1,53 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class AphrontMySQLDatabaseConnectionTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
// We disable this here because we're testing live MySQL connections.
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => false,
);
}
public function testConnectionFailures() {
$conn = id(new PhabricatorPHID())->establishConnection('r');
queryfx($conn, 'SELECT 1');
// We expect the connection to recover from a 2006 (lost connection) when
// outside of a transaction...
$conn->simulateErrorOnNextQuery(2006);
queryfx($conn, 'SELECT 1');
// ...but when transactional, we expect the query to throw when the
// connection is lost, because it indicates the transaction was aborted.
$conn->openTransaction();
$conn->simulateErrorOnNextQuery(2006);
$caught = null;
try {
queryfx($conn, 'SELECT 1');
} catch (AphrontQueryConnectionLostException $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/phid/storage/phid');
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phutil', 'utils');
phutil_require_source('AphrontMySQLDatabaseConnectionTestCase.php');