Skip to content

Commit c30cb76

Browse files
author
epriestley
committed
Simplify transaction handling and restore read/write locks
Summary: - We used to have connection-level caching, so we needed getTransactionKey() to make sure there was one transaction state per real connection. We now cache in Lisk and each Connection object is guaranteed to represent a real, unique connection, so we can make this a non-static. - I kept the classes separate because it was a little easier, but maybe we should merge them? - Also track/implement read/write locking. - (The advantage of this over just writing LOCK IN SHARE MODE is that you can use, e.g., some Query class even if you don't have access to the queries it runs.) Test Plan: Can you come up with a way to write unit tests for this? It seems like testing that it works requires deadlocking MySQL if the test is running in one process. Reviewers: vrana, btrahan Reviewed By: vrana CC: aran Differential Revision: https://secure.phabricator.com/D2398
1 parent 9f9716f commit c30cb76

File tree

11 files changed

+210
-32
lines changed

11 files changed

+210
-32
lines changed

src/__phutil_library_map__.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
'AphrontQueryConnectionException' => 'storage/exception/connection',
8080
'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
8181
'AphrontQueryCountException' => 'storage/exception/count',
82+
'AphrontQueryDeadlockException' => 'storage/exception/deadlock',
8283
'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey',
8384
'AphrontQueryException' => 'storage/exception/base',
8485
'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
@@ -182,9 +183,9 @@
182183
'ConduitAPI_slowvote_info_Method' => 'applications/conduit/method/slowvote/info',
183184
'ConduitAPI_user_Method' => 'applications/conduit/method/user/base',
184185
'ConduitAPI_user_addstatus_Method' => 'applications/conduit/method/user/addstatus',
185-
'ConduitAPI_user_removestatus_Method' => 'applications/conduit/method/user/removestatus',
186186
'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find',
187187
'ConduitAPI_user_info_Method' => 'applications/conduit/method/user/info',
188+
'ConduitAPI_user_removestatus_Method' => 'applications/conduit/method/user/removestatus',
188189
'ConduitAPI_user_whoami_Method' => 'applications/conduit/method/user/whoami',
189190
'ConduitException' => 'applications/conduit/protocol/exception',
190191
'DarkConsoleConfigPlugin' => 'aphront/console/plugin/config',
@@ -1118,6 +1119,7 @@
11181119
'AphrontQueryConnectionException' => 'AphrontQueryException',
11191120
'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
11201121
'AphrontQueryCountException' => 'AphrontQueryException',
1122+
'AphrontQueryDeadlockException' => 'AphrontQueryRecoverableException',
11211123
'AphrontQueryDuplicateKeyException' => 'AphrontQueryException',
11221124
'AphrontQueryObjectMissingException' => 'AphrontQueryException',
11231125
'AphrontQueryParameterException' => 'AphrontQueryException',
@@ -1207,9 +1209,9 @@
12071209
'ConduitAPI_slowvote_info_Method' => 'ConduitAPIMethod',
12081210
'ConduitAPI_user_Method' => 'ConduitAPIMethod',
12091211
'ConduitAPI_user_addstatus_Method' => 'ConduitAPI_user_Method',
1210-
'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method',
12111212
'ConduitAPI_user_find_Method' => 'ConduitAPI_user_Method',
12121213
'ConduitAPI_user_info_Method' => 'ConduitAPI_user_Method',
1214+
'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method',
12131215
'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method',
12141216
'DarkConsoleConfigPlugin' => 'DarkConsolePlugin',
12151217
'DarkConsoleController' => 'PhabricatorController',

src/storage/connection/base/AphrontDatabaseConnection.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
*/
2323
abstract class AphrontDatabaseConnection {
2424

25-
private static $transactionStates = array();
25+
private $transactionState;
2626

2727
abstract public function getInsertID();
2828
abstract public function getAffectedRows();
2929
abstract public function selectAllResults();
3030
abstract public function executeRawQuery($raw_query);
31-
abstract protected function getTransactionKey();
3231

3332
abstract public function escapeString($string);
3433
abstract public function escapeColumnName($string);
@@ -133,11 +132,62 @@ public function isInsideTransaction() {
133132
* @task xaction
134133
*/
135134
protected function getTransactionState() {
136-
$key = $this->getTransactionKey();
137-
if (empty(self::$transactionStates[$key])) {
138-
self::$transactionStates[$key] = new AphrontDatabaseTransactionState();
135+
if (!$this->transactionState) {
136+
$this->transactionState = new AphrontDatabaseTransactionState();
139137
}
140-
return self::$transactionStates[$key];
138+
return $this->transactionState;
139+
}
140+
141+
142+
/**
143+
* @task xaction
144+
*/
145+
public function beginReadLocking() {
146+
$this->getTransactionState()->beginReadLocking();
147+
return $this;
148+
}
149+
150+
151+
/**
152+
* @task xaction
153+
*/
154+
public function endReadLocking() {
155+
$this->getTransactionState()->endReadLocking();
156+
return $this;
157+
}
158+
159+
160+
/**
161+
* @task xaction
162+
*/
163+
public function isReadLocking() {
164+
return $this->getTransactionState()->isReadLocking();
165+
}
166+
167+
168+
/**
169+
* @task xaction
170+
*/
171+
public function beginWriteLocking() {
172+
$this->getTransactionState()->beginWriteLocking();
173+
return $this;
174+
}
175+
176+
177+
/**
178+
* @task xaction
179+
*/
180+
public function endWriteLocking() {
181+
$this->getTransactionState()->endWriteLocking();
182+
return $this;
183+
}
184+
185+
186+
/**
187+
* @task xaction
188+
*/
189+
public function isWriteLocking() {
190+
return $this->getTransactionState()->isWriteLocking();
141191
}
142192

143193
}

src/storage/connection/isolated/AphrontIsolatedDatabaseConnection.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ final class AphrontIsolatedDatabaseConnection
2525
private $configuration;
2626
private static $nextInsertID;
2727
private $insertID;
28-
private static $nextTransactionKey = 1;
29-
private $transactionKey;
3028

3129
private $transcript = array();
3230

@@ -38,8 +36,6 @@ public function __construct(array $configuration) {
3836
// collisions and make them distinctive.
3937
self::$nextInsertID = 55555000000 + mt_rand(0, 1000);
4038
}
41-
42-
$this->transactionKey = 'iso-xaction-'.(self::$nextTransactionKey++);
4339
}
4440

4541
public function escapeString($string) {
@@ -70,10 +66,6 @@ public function getAffectedRows() {
7066
return $this->affectedRows;
7167
}
7268

73-
protected function getTransactionKey() {
74-
return $this->transactionKey;
75-
}
76-
7769
public function selectAllResults() {
7870
return $this->allResults;
7971
}

src/storage/connection/mysql/base/AphrontMySQLDatabaseConnectionBase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ protected function throwQueryException($connection) {
225225
throw new AphrontQueryConnectionLostException($exmsg);
226226
case 1213: // Deadlock
227227
case 1205: // Lock wait timeout exceeded
228-
throw new AphrontQueryRecoverableException($exmsg);
228+
throw new AphrontQueryDeadlockException($exmsg);
229229
case 1062: // Duplicate Key
230230
// NOTE: In some versions of MySQL we get a key name back here, but
231231
// older versions just give us a key index ("key 2") so it's not

src/storage/connection/mysql/base/__init__.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
phutil_require_module('phabricator', 'storage/exception/accessdenied');
1313
phutil_require_module('phabricator', 'storage/exception/base');
1414
phutil_require_module('phabricator', 'storage/exception/connectionlost');
15+
phutil_require_module('phabricator', 'storage/exception/deadlock');
1516
phutil_require_module('phabricator', 'storage/exception/duplicatekey');
16-
phutil_require_module('phabricator', 'storage/exception/recoverable');
1717
phutil_require_module('phabricator', 'storage/exception/schema');
1818

1919
phutil_require_module('phutil', 'error');

src/storage/connection/mysql/mysql/AphrontMySQLDatabaseConnection.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ public function getAffectedRows() {
3434
return mysql_affected_rows($this->requireConnection());
3535
}
3636

37-
protected function getTransactionKey() {
38-
return (int)$this->requireConnection();
39-
}
40-
4137
protected function connect() {
4238
if (!function_exists('mysql_connect')) {
4339
// We have to '@' the actual call since it can spew all sorts of silly

src/storage/connection/mysql/mysqli/AphrontMySQLiDatabaseConnection.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ public function getAffectedRows() {
3636
return $this->requireConnection()->affected_rows;
3737
}
3838

39-
protected function getTransactionKey() {
40-
return spl_object_hash($this->requireConnection());
41-
}
42-
4339
protected function connect() {
4440
if (!class_exists('mysqli', false)) {
4541
throw new Exception(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2012 Facebook, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* @group storage
21+
*/
22+
final class AphrontQueryDeadlockException
23+
extends AphrontQueryRecoverableException { }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/**
3+
* This file is automatically generated. Lint this module to rebuild it.
4+
* @generated
5+
*/
6+
7+
8+
9+
phutil_require_module('phabricator', 'storage/exception/recoverable');
10+
11+
12+
phutil_require_source('AphrontQueryDeadlockException.php');

src/storage/lisk/dao/LiskDAO.php

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,16 +542,11 @@ protected function loadRawDataWhere($columns, $pattern/*, $args... */) {
542542
$connection = $this->establishConnection('r');
543543

544544
$lock_clause = '';
545-
/*
546-
547-
TODO: Restore this?
548-
549545
if ($connection->isReadLocking()) {
550546
$lock_clause = 'FOR UPDATE';
551547
} else if ($connection->isWriteLocking()) {
552548
$lock_clause = 'LOCK IN SHARE MODE';
553549
}
554-
*/
555550

556551
$args = func_get_args();
557552
$args = array_slice($args, 2);
@@ -1342,8 +1337,74 @@ public function killTransaction() {
13421337
}
13431338

13441339

1340+
/**
1341+
* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
1342+
* other connections can not read them (this is an enormous oversimplification
1343+
* of FOR UPDATE semantics; consult the MySQL documentation for details). To
1344+
* end read locking, call @{method:endReadLocking}. For example:
1345+
*
1346+
* $beach->openTransaction();
1347+
* $beach->beginReadLocking();
1348+
*
1349+
* $beach->reload();
1350+
* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
1351+
* $beach->save();
1352+
*
1353+
* $beach->endReadLocking();
1354+
* $beach->saveTransaction();
1355+
*
1356+
* @return this
1357+
* @task xaction
1358+
*/
1359+
public function beginReadLocking() {
1360+
$this->establishConnection('w')->beginReadLocking();
1361+
return $this;
1362+
}
1363+
1364+
1365+
/**
1366+
* Ends read-locking that began at an earlier @{method:beginReadLocking} call.
1367+
*
1368+
* @return this
1369+
* @task xaction
1370+
*/
1371+
public function endReadLocking() {
1372+
$this->establishConnection('w')->endReadLocking();
1373+
return $this;
1374+
}
1375+
1376+
/**
1377+
* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
1378+
* that other connections can not update or delete them (this is an
1379+
* oversimplification of LOCK IN SHARE MODE semantics; consult the
1380+
* MySQL documentation for details). To end write locking, call
1381+
* @{method:endWriteLocking}.
1382+
*
1383+
* @return this
1384+
* @task xaction
1385+
*/
1386+
public function beginWriteLocking() {
1387+
$this->establishConnection('w')->beginWriteLocking();
1388+
return $this;
1389+
}
1390+
1391+
1392+
/**
1393+
* Ends write-locking that began at an earlier @{method:beginWriteLocking}
1394+
* call.
1395+
*
1396+
* @return this
1397+
* @task xaction
1398+
*/
1399+
public function endWriteLocking() {
1400+
$this->establishConnection('w')->endWriteLocking();
1401+
return $this;
1402+
}
1403+
1404+
13451405
/* -( Isolation )---------------------------------------------------------- */
13461406

1407+
13471408
/**
13481409
* @task isolate
13491410
*/

0 commit comments

Comments
 (0)