mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +01:00
Restore Lisk transactional methods
Summary: Restores a (simplified and improved) version of Lisk transactions. This doesn't actually use transactions anywhere yet. DifferentialRevisionEditor is the #1 (and only?) case where we have transaction problems right now, but sticking save() inside a transaction unconditionally will leave us holding a transaction open for like a million years while we run Herald rules, etc. I want to do some refactoring there separately from this diff before making it transactional. NOTE: @jungejason / @nh, can one of you verify these unit tests pass on HPHP/i/vm when you get a chance? I vaguely recall there was some problem with (int)$resource. We can land this safely without verifying that, but should check before we start using it anywhere. Test Plan: Ran unit tests. Reviewers: btrahan, nh, jungejason Reviewed By: btrahan CC: aran, epriestley Maniphest Tasks: T605 Differential Revision: https://secure.phabricator.com/D1515
This commit is contained in:
parent
2c3a08b37b
commit
4f018488ae
9 changed files with 361 additions and 180 deletions
|
@ -21,6 +21,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontController' => 'aphront/controller',
|
'AphrontController' => 'aphront/controller',
|
||||||
'AphrontCrumbsView' => 'view/layout/crumbs',
|
'AphrontCrumbsView' => 'view/layout/crumbs',
|
||||||
'AphrontDatabaseConnection' => 'storage/connection/base',
|
'AphrontDatabaseConnection' => 'storage/connection/base',
|
||||||
|
'AphrontDatabaseTransactionState' => 'storage/transaction',
|
||||||
'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
|
'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
|
||||||
'AphrontDefaultApplicationController' => 'aphront/default/controller',
|
'AphrontDefaultApplicationController' => 'aphront/default/controller',
|
||||||
'AphrontDialogResponse' => 'aphront/response/dialog',
|
'AphrontDialogResponse' => 'aphront/response/dialog',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,12 +17,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @task xaction Transaction Management
|
||||||
* @group storage
|
* @group storage
|
||||||
*/
|
*/
|
||||||
abstract class AphrontDatabaseConnection {
|
abstract class AphrontDatabaseConnection {
|
||||||
|
|
||||||
private static $transactionStacks = array();
|
private static $transactionStates = array();
|
||||||
private static $transactionShutdownRegistered = false;
|
private $initializingTransactionState;
|
||||||
|
|
||||||
abstract public function getInsertID();
|
abstract public function getInsertID();
|
||||||
abstract public function getAffectedRows();
|
abstract public function getAffectedRows();
|
||||||
|
@ -47,179 +48,107 @@ abstract class AphrontDatabaseConnection {
|
||||||
return call_user_func_array('queryfx', $args);
|
return call_user_func_array('queryfx', $args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Probably need to reset these when we catch a connection exception
|
|
||||||
// in the transaction stack.
|
|
||||||
protected function &getLockLevels() {
|
|
||||||
static $levels = array();
|
|
||||||
$key = $this->getTransactionKey();
|
|
||||||
if (!isset($levels[$key])) {
|
|
||||||
$levels[$key] = array(
|
|
||||||
'read' => 0,
|
|
||||||
'write' => 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $levels[$key];
|
/* -( Transaction Management )--------------------------------------------- */
|
||||||
}
|
|
||||||
|
|
||||||
public function isReadLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
return ($levels['read'] > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isWriteLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
return ($levels['write'] > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function startReadLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
++$levels['read'];
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function startWriteLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
++$levels['write'];
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function stopReadLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
if ($levels['read'] < 1) {
|
|
||||||
throw new Exception('Unable to stop read locking: not read locking.');
|
|
||||||
}
|
|
||||||
--$levels['read'];
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function stopWriteLocking() {
|
|
||||||
$levels = &$this->getLockLevels();
|
|
||||||
if ($levels['write'] < 1) {
|
|
||||||
throw new Exception('Unable to stop read locking: not write locking.');
|
|
||||||
}
|
|
||||||
--$levels['write'];
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function &getTransactionStack($key) {
|
|
||||||
if (!self::$transactionShutdownRegistered) {
|
|
||||||
self::$transactionShutdownRegistered = true;
|
|
||||||
register_shutdown_function(
|
|
||||||
array(
|
|
||||||
'AphrontDatabaseConnection',
|
|
||||||
'shutdownTransactionStacks',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset(self::$transactionStacks[$key])) {
|
|
||||||
self::$transactionStacks[$key] = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$transactionStacks[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function shutdownTransactionStacks() {
|
|
||||||
foreach (self::$transactionStacks as $stack) {
|
|
||||||
if ($stack === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = count($stack);
|
|
||||||
if ($count) {
|
|
||||||
throw new Exception(
|
|
||||||
'Script exited with '.$count.' open transactions! The '.
|
|
||||||
'transactions will be implicitly rolled back. Calls to '.
|
|
||||||
'openTransaction() should always be paired with a call to '.
|
|
||||||
'saveTransaction() or killTransaction(); you have an unpaired '.
|
|
||||||
'call somewhere.',
|
|
||||||
$count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin a transaction, or set a savepoint if the connection is already
|
||||||
|
* transactional.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
* @task xaction
|
||||||
|
*/
|
||||||
public function openTransaction() {
|
public function openTransaction() {
|
||||||
$key = $this->getTransactionKey();
|
$state = $this->getTransactionState();
|
||||||
$stack = &$this->getTransactionStack($key);
|
$point = $state->getSavepointName();
|
||||||
|
$depth = $state->increaseDepth();
|
||||||
$new_transaction = !count($stack);
|
|
||||||
|
|
||||||
// TODO: At least in development, push context information instead of
|
|
||||||
// `true' so we can report (or, at least, guess) where unpaired
|
|
||||||
// transaction calls happened.
|
|
||||||
$stack[] = true;
|
|
||||||
|
|
||||||
end($stack);
|
|
||||||
$key = key($stack);
|
|
||||||
|
|
||||||
|
$new_transaction = ($depth == 1);
|
||||||
if ($new_transaction) {
|
if ($new_transaction) {
|
||||||
$this->query('START TRANSACTION');
|
$this->query('START TRANSACTION');
|
||||||
} else {
|
} else {
|
||||||
$this->query('SAVEPOINT '.$this->getSavepointName($key));
|
$this->query('SAVEPOINT '.$point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isInsideTransaction() {
|
|
||||||
$key = $this->getTransactionKey();
|
|
||||||
$stack = &$this->getTransactionStack($key);
|
|
||||||
return (bool)count($stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit a transaction, or stage a savepoint for commit once the entire
|
||||||
|
* transaction completes if inside a transaction stack.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
* @task xaction
|
||||||
|
*/
|
||||||
public function saveTransaction() {
|
public function saveTransaction() {
|
||||||
$key = $this->getTransactionKey();
|
$state = $this->getTransactionState();
|
||||||
$stack = &$this->getTransactionStack($key);
|
$depth = $state->decreaseDepth();
|
||||||
|
|
||||||
if (!count($stack)) {
|
if ($depth == 0) {
|
||||||
throw new Exception(
|
|
||||||
"No open transaction! Unable to save transaction, since there ".
|
|
||||||
"isn't one.");
|
|
||||||
}
|
|
||||||
|
|
||||||
array_pop($stack);
|
|
||||||
|
|
||||||
if (!count($stack)) {
|
|
||||||
$this->query('COMMIT');
|
$this->query('COMMIT');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveTransactionUnless($cond) {
|
|
||||||
if ($cond) {
|
|
||||||
$this->killTransaction();
|
|
||||||
} else {
|
|
||||||
$this->saveTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveTransactionIf($cond) {
|
|
||||||
$this->saveTransactionUnless(!$cond);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback a transaction, or unstage the last savepoint if inside a
|
||||||
|
* transaction stack.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
public function killTransaction() {
|
public function killTransaction() {
|
||||||
$key = $this->getTransactionKey();
|
$state = $this->getTransactionState();
|
||||||
$stack = &$this->getTransactionStack($key);
|
$depth = $state->decreaseDepth();
|
||||||
|
|
||||||
if (!count($stack)) {
|
if ($depth == 0) {
|
||||||
throw new Exception(
|
|
||||||
"No open transaction! Unable to kill transaction, since there ".
|
|
||||||
"isn't one.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = count($stack);
|
|
||||||
|
|
||||||
end($stack);
|
|
||||||
$key = key($stack);
|
|
||||||
array_pop($stack);
|
|
||||||
|
|
||||||
if (!count($stack)) {
|
|
||||||
$this->query('ROLLBACK');
|
$this->query('ROLLBACK');
|
||||||
} else {
|
} else {
|
||||||
$this->query(
|
$this->query('ROLLBACK TO SAVEPOINT '.$state->getSavepointName());
|
||||||
'ROLLBACK TO SAVEPOINT '.$this->getSavepointName($key)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getSavepointName($key) {
|
|
||||||
return 'LiskSavepoint_'.$key;
|
/**
|
||||||
|
* Returns true if the connection is transactional.
|
||||||
|
*
|
||||||
|
* @return bool True if the connection is currently transactional.
|
||||||
|
* @task xaction
|
||||||
|
*/
|
||||||
|
public function isInsideTransaction() {
|
||||||
|
if ($this->initializingTransactionState) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$state = $this->getTransactionState();
|
||||||
|
return ($state->getDepth() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current @{class:AphrontDatabaseTransactionState} object, or create
|
||||||
|
* one if none exists.
|
||||||
|
*
|
||||||
|
* @return AphrontDatabaseTransactionState Current transaction state.
|
||||||
|
* @task xaction
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
if (empty(self::$transactionStates[$key])) {
|
||||||
|
self::$transactionStates[$key] = new AphrontDatabaseTransactionState();
|
||||||
|
}
|
||||||
|
$this->initializingTransactionState = false;
|
||||||
|
|
||||||
|
return self::$transactionStates[$key];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
|
|
||||||
phutil_require_module('phabricator', 'storage/queryfx');
|
phutil_require_module('phabricator', 'storage/queryfx');
|
||||||
|
phutil_require_module('phabricator', 'storage/transaction');
|
||||||
|
|
||||||
|
|
||||||
phutil_require_source('AphrontDatabaseConnection.php');
|
phutil_require_source('AphrontDatabaseConnection.php');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -24,6 +24,10 @@ class AphrontIsolatedDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
private $configuration;
|
private $configuration;
|
||||||
private static $nextInsertID;
|
private static $nextInsertID;
|
||||||
private $insertID;
|
private $insertID;
|
||||||
|
private static $nextTransactionKey = 1;
|
||||||
|
private $transactionKey;
|
||||||
|
|
||||||
|
private $transcript = array();
|
||||||
|
|
||||||
public function __construct(array $configuration) {
|
public function __construct(array $configuration) {
|
||||||
$this->configuration = $configuration;
|
$this->configuration = $configuration;
|
||||||
|
@ -33,6 +37,8 @@ class AphrontIsolatedDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
// collisions and make them distinctive.
|
// collisions and make them distinctive.
|
||||||
self::$nextInsertID = 55555000000 + mt_rand(0, 1000);
|
self::$nextInsertID = 55555000000 + mt_rand(0, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->transactionKey = 'iso-xaction-'.(self::$nextTransactionKey++);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function escapeString($string) {
|
public function escapeString($string) {
|
||||||
|
@ -64,7 +70,7 @@ class AphrontIsolatedDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTransactionKey() {
|
public function getTransactionKey() {
|
||||||
return 'xaction'; // TODO, probably need to stub this better.
|
return $this->transactionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function selectAllResults() {
|
public function selectAllResults() {
|
||||||
|
@ -74,17 +80,35 @@ class AphrontIsolatedDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
public function executeRawQuery($raw_query) {
|
public function executeRawQuery($raw_query) {
|
||||||
|
|
||||||
// NOTE: "[\s<>K]*" allows any number of (properly escaped) comments to
|
// NOTE: "[\s<>K]*" allows any number of (properly escaped) comments to
|
||||||
// appear prior to the INSERT/UPDATE/DELETE, since this connection escapes
|
// appear prior to the allowed keyword, since this connection escapes
|
||||||
// them as "<K>" (above).
|
// them as "<K>" (above).
|
||||||
if (!preg_match('/^[\s<>K]*(INSERT|UPDATE|DELETE)\s*/i', $raw_query)) {
|
|
||||||
|
$keywords = array(
|
||||||
|
'INSERT',
|
||||||
|
'UPDATE',
|
||||||
|
'DELETE',
|
||||||
|
'START',
|
||||||
|
'SAVEPOINT',
|
||||||
|
'COMMIT',
|
||||||
|
'ROLLBACK',
|
||||||
|
);
|
||||||
|
$preg_keywords = array();
|
||||||
|
foreach ($keywords as $key => $word) {
|
||||||
|
$preg_keywords[] = preg_quote($word, '/');
|
||||||
|
}
|
||||||
|
$preg_keywords = implode('|', $preg_keywords);
|
||||||
|
|
||||||
|
if (!preg_match('/^[\s<>K]*('.$preg_keywords.')\s*/i', $raw_query)) {
|
||||||
$doc_uri = PhabricatorEnv::getDoclink('article/Writing_Unit_Tests.html');
|
$doc_uri = PhabricatorEnv::getDoclink('article/Writing_Unit_Tests.html');
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
"Database isolation currently only supports INSERT, UPDATE and DELETE ".
|
"Database isolation currently only supports some queries. For more ".
|
||||||
"queries. For more information, see <{$doc_uri}>. You are trying to ".
|
"information, see <{$doc_uri}>. You are trying to issue a query which ".
|
||||||
"issue a query which does not begin with INSERT, UPDATE or DELETE: ".
|
"does not begin with an allowed keyword ".
|
||||||
"'".$raw_query."'");
|
"(".implode(', ', $keywords)."): '".$raw_query."'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->transcript[] = $raw_query;
|
||||||
|
|
||||||
// NOTE: This method is intentionally simplified for now, since we're only
|
// NOTE: This method is intentionally simplified for now, since we're only
|
||||||
// using it to stub out inserts/updates. In the future it will probably need
|
// using it to stub out inserts/updates. In the future it will probably need
|
||||||
// to grow more powerful.
|
// to grow more powerful.
|
||||||
|
@ -99,4 +123,8 @@ class AphrontIsolatedDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
$this->affectedRows = 1;
|
$this->affectedRows = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getQueryTranscript() {
|
||||||
|
return $this->transcript;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -30,27 +30,14 @@ class AphrontIsolatedDatabaseConnectionTestCase
|
||||||
|
|
||||||
public function testIsolation() {
|
public function testIsolation() {
|
||||||
$conn = $this->newIsolatedConnection();
|
$conn = $this->newIsolatedConnection();
|
||||||
|
$test_phid = $this->generateTestPHID();
|
||||||
$test_phid = 'PHID-TEST-'.Filesystem::readRandomCharacters(20);
|
|
||||||
|
|
||||||
queryfx(
|
queryfx(
|
||||||
$conn,
|
$conn,
|
||||||
'INSERT INTO phabricator_phid.phid (phid) VALUES (%s)',
|
'INSERT INTO phabricator_phid.phid (phid) VALUES (%s)',
|
||||||
$test_phid);
|
$test_phid);
|
||||||
|
|
||||||
try {
|
$this->assertNoSuchPHID($test_phid);
|
||||||
$real_phid = id(new PhabricatorPHID())->loadOneWhere(
|
|
||||||
'phid = %s',
|
|
||||||
$test_phid);
|
|
||||||
$this->assertEqual(
|
|
||||||
null,
|
|
||||||
$real_phid,
|
|
||||||
'Expect fake PHID to exist only in isolation.');
|
|
||||||
} catch (AphrontQueryConnectionException $ex) {
|
|
||||||
// If we can't connect to the database, conclude that the isolated
|
|
||||||
// connection actually is isolated. Philosophically, this perhaps allows
|
|
||||||
// us to claim this test does not depend on the database?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testInsertGeneratesID() {
|
public function testInsertGeneratesID() {
|
||||||
|
@ -75,8 +62,103 @@ class AphrontIsolatedDatabaseConnectionTestCase
|
||||||
queryfx($conn, 'DELETE');
|
queryfx($conn, 'DELETE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTransactionStack() {
|
||||||
|
$conn = $this->newIsolatedConnection();
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT');
|
||||||
|
$conn->saveTransaction();
|
||||||
|
$this->assertEqual(
|
||||||
|
array(
|
||||||
|
'START TRANSACTION',
|
||||||
|
'INSERT',
|
||||||
|
'COMMIT',
|
||||||
|
),
|
||||||
|
$conn->getQueryTranscript());
|
||||||
|
|
||||||
|
$conn = $this->newIsolatedConnection();
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT 1');
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT 2');
|
||||||
|
$conn->killTransaction();
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT 3');
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT 4');
|
||||||
|
$conn->saveTransaction();
|
||||||
|
$conn->saveTransaction();
|
||||||
|
$conn->openTransaction();
|
||||||
|
queryfx($conn, 'INSERT 5');
|
||||||
|
$conn->killTransaction();
|
||||||
|
queryfx($conn, 'INSERT 6');
|
||||||
|
$conn->saveTransaction();
|
||||||
|
|
||||||
|
$this->assertEqual(
|
||||||
|
array(
|
||||||
|
'START TRANSACTION',
|
||||||
|
'INSERT 1',
|
||||||
|
'SAVEPOINT Aphront_Savepoint_1',
|
||||||
|
'INSERT 2',
|
||||||
|
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
|
||||||
|
'SAVEPOINT Aphront_Savepoint_1',
|
||||||
|
'INSERT 3',
|
||||||
|
'SAVEPOINT Aphront_Savepoint_2',
|
||||||
|
'INSERT 4',
|
||||||
|
'SAVEPOINT Aphront_Savepoint_1',
|
||||||
|
'INSERT 5',
|
||||||
|
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
|
||||||
|
'INSERT 6',
|
||||||
|
'COMMIT',
|
||||||
|
),
|
||||||
|
$conn->getQueryTranscript());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransactionRollback() {
|
||||||
|
$check = array();
|
||||||
|
|
||||||
|
$phid = new PhabricatorPHID();
|
||||||
|
$phid->openTransaction();
|
||||||
|
for ($ii = 0; $ii < 3; $ii++) {
|
||||||
|
$test_phid = $this->generateTestPHID();
|
||||||
|
|
||||||
|
$obj = new PhabricatorPHID();
|
||||||
|
$obj->setPHID($test_phid);
|
||||||
|
$obj->setPHIDType('TEST');
|
||||||
|
$obj->setOwnerPHID('PHID-UNIT-!!!!');
|
||||||
|
$obj->save();
|
||||||
|
|
||||||
|
$check[] = $test_phid;
|
||||||
|
}
|
||||||
|
$phid->killTransaction();
|
||||||
|
|
||||||
|
foreach ($check as $test_phid) {
|
||||||
|
$this->assertNoSuchPHID($test_phid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function newIsolatedConnection() {
|
private function newIsolatedConnection() {
|
||||||
$config = array();
|
$config = array();
|
||||||
return new AphrontIsolatedDatabaseConnection($config);
|
return new AphrontIsolatedDatabaseConnection($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateTestPHID() {
|
||||||
|
return 'PHID-TEST-'.Filesystem::readRandomCharacters(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertNoSuchPHID($phid) {
|
||||||
|
try {
|
||||||
|
$real_phid = id(new PhabricatorPHID())->loadOneWhere(
|
||||||
|
'phid = %s',
|
||||||
|
$phid);
|
||||||
|
$this->assertEqual(
|
||||||
|
null,
|
||||||
|
$real_phid,
|
||||||
|
'Expect fake PHID to exist only in isolation.');
|
||||||
|
} catch (AphrontQueryConnectionException $ex) {
|
||||||
|
// If we can't connect to the database, conclude that the isolated
|
||||||
|
// connection actually is isolated. Philosophically, this perhaps allows
|
||||||
|
// us to claim this test does not depend on the database?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,16 +107,17 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
private function establishConnection() {
|
private function establishConnection() {
|
||||||
$this->closeConnection();
|
$this->closeConnection();
|
||||||
|
|
||||||
$user = $this->getConfiguration('user');
|
|
||||||
$host = $this->getConfiguration('host');
|
|
||||||
$database = $this->getConfiguration('database');
|
|
||||||
|
|
||||||
$key = $this->getConnectionCacheKey();
|
$key = $this->getConnectionCacheKey();
|
||||||
if (isset(self::$connectionCache[$key])) {
|
if (isset(self::$connectionCache[$key])) {
|
||||||
$this->connection = self::$connectionCache[$key];
|
$this->connection = self::$connectionCache[$key];
|
||||||
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')) {
|
||||||
|
@ -129,6 +130,11 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
"available!");
|
"available!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = $this->getConfiguration('user');
|
||||||
|
$host = $this->getConfiguration('host');
|
||||||
|
$database = $this->getConfiguration('database');
|
||||||
|
|
||||||
|
|
||||||
$profiler = PhutilServiceProfiler::getInstance();
|
$profiler = PhutilServiceProfiler::getInstance();
|
||||||
$call_id = $profiler->beginServiceCall(
|
$call_id = $profiler->beginServiceCall(
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -135,12 +135,37 @@
|
||||||
* loadAllWhere() returns a list of objects, while loadOneWhere() returns a
|
* loadAllWhere() returns a list of objects, while loadOneWhere() returns a
|
||||||
* single object (or null).
|
* single object (or null).
|
||||||
*
|
*
|
||||||
|
* = Managing Transactions =
|
||||||
|
*
|
||||||
|
* Lisk uses a transaction stack, so code does not generally need to be aware
|
||||||
|
* of the transactional state of objects to implement correct transaction
|
||||||
|
* semantics:
|
||||||
|
*
|
||||||
|
* $obj->openTransaction();
|
||||||
|
* $obj->save();
|
||||||
|
* $other->save();
|
||||||
|
* // ...
|
||||||
|
* $other->openTransaction();
|
||||||
|
* $other->save();
|
||||||
|
* $another->save();
|
||||||
|
* if ($some_condition) {
|
||||||
|
* $other->saveTransaction();
|
||||||
|
* } else {
|
||||||
|
* $other->killTransaction();
|
||||||
|
* }
|
||||||
|
* // ...
|
||||||
|
* $obj->saveTransaction();
|
||||||
|
*
|
||||||
|
* Assuming ##$obj##, ##$other## and ##$another## live on the same database,
|
||||||
|
* this code will work correctly by establishing savepoints.
|
||||||
|
*
|
||||||
* @task config Configuring Lisk
|
* @task config Configuring Lisk
|
||||||
* @task load Loading Objects
|
* @task load Loading Objects
|
||||||
* @task info Examining Objects
|
* @task info Examining Objects
|
||||||
* @task save Writing Objects
|
* @task save Writing Objects
|
||||||
* @task hook Hooks and Callbacks
|
* @task hook Hooks and Callbacks
|
||||||
* @task util Utilities
|
* @task util Utilities
|
||||||
|
* @task xaction Managing Transactions
|
||||||
* @task isolate Isolation for Unit Testing
|
* @task isolate Isolation for Unit Testing
|
||||||
*
|
*
|
||||||
* @group storage
|
* @group storage
|
||||||
|
@ -454,11 +479,16 @@ abstract class LiskDAO {
|
||||||
$connection = $this->establishConnection('r');
|
$connection = $this->establishConnection('r');
|
||||||
|
|
||||||
$lock_clause = '';
|
$lock_clause = '';
|
||||||
|
/*
|
||||||
|
|
||||||
|
TODO: Restore this?
|
||||||
|
|
||||||
if ($connection->isReadLocking()) {
|
if ($connection->isReadLocking()) {
|
||||||
$lock_clause = 'FOR UPDATE';
|
$lock_clause = 'FOR UPDATE';
|
||||||
} else if ($connection->isWriteLocking()) {
|
} else if ($connection->isWriteLocking()) {
|
||||||
$lock_clause = 'LOCK IN SHARE MODE';
|
$lock_clause = 'LOCK IN SHARE MODE';
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
$args = func_get_args();
|
$args = func_get_args();
|
||||||
$args = array_slice($args, 2);
|
$args = array_slice($args, 2);
|
||||||
|
@ -1165,6 +1195,42 @@ abstract class LiskDAO {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Manging Transactions )----------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase transaction stack depth.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function openTransaction() {
|
||||||
|
$this->establishConnection('w')->openTransaction();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrease transaction stack depth, saving work.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function saveTransaction() {
|
||||||
|
$this->establishConnection('w')->saveTransaction();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrease transaction stack depth, discarding work.
|
||||||
|
*
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function killTransaction() {
|
||||||
|
$this->establishConnection('w')->killTransaction();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Isolation )---------------------------------------------------------- */
|
/* -( Isolation )---------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
58
src/storage/transaction/AphrontDatabaseTransactionState.php
Normal file
58
src/storage/transaction/AphrontDatabaseTransactionState.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents current transaction state of a connection.
|
||||||
|
*
|
||||||
|
* @group storage
|
||||||
|
*/
|
||||||
|
final class AphrontDatabaseTransactionState {
|
||||||
|
|
||||||
|
private $depth;
|
||||||
|
|
||||||
|
public function getDepth() {
|
||||||
|
return $this->depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function increaseDepth() {
|
||||||
|
return ++$this->depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decreaseDepth() {
|
||||||
|
if ($this->depth == 0) {
|
||||||
|
throw new Exception(
|
||||||
|
'Too many calls to saveTransaction() or killTransaction()!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return --$this->depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSavepointName() {
|
||||||
|
return 'Aphront_Savepoint_'.$this->depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
if ($this->depth) {
|
||||||
|
throw new Exception(
|
||||||
|
'Process exited with an open transaction! The transaction will be '.
|
||||||
|
'implicitly rolled back. Calls to openTransaction() must always be '.
|
||||||
|
'paired with a call to saveTransaction() or killTransaction().');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
10
src/storage/transaction/__init__.php
Normal file
10
src/storage/transaction/__init__.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('AphrontDatabaseTransactionState.php');
|
Loading…
Reference in a new issue