@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+210 -32
+4 -2
src/__phutil_library_map__.php
··· 79 79 'AphrontQueryConnectionException' => 'storage/exception/connection', 80 80 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 81 81 'AphrontQueryCountException' => 'storage/exception/count', 82 + 'AphrontQueryDeadlockException' => 'storage/exception/deadlock', 82 83 'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey', 83 84 'AphrontQueryException' => 'storage/exception/base', 84 85 'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing', ··· 182 183 'ConduitAPI_slowvote_info_Method' => 'applications/conduit/method/slowvote/info', 183 184 'ConduitAPI_user_Method' => 'applications/conduit/method/user/base', 184 185 'ConduitAPI_user_addstatus_Method' => 'applications/conduit/method/user/addstatus', 185 - 'ConduitAPI_user_removestatus_Method' => 'applications/conduit/method/user/removestatus', 186 186 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find', 187 187 'ConduitAPI_user_info_Method' => 'applications/conduit/method/user/info', 188 + 'ConduitAPI_user_removestatus_Method' => 'applications/conduit/method/user/removestatus', 188 189 'ConduitAPI_user_whoami_Method' => 'applications/conduit/method/user/whoami', 189 190 'ConduitException' => 'applications/conduit/protocol/exception', 190 191 'DarkConsoleConfigPlugin' => 'aphront/console/plugin/config', ··· 1118 1119 'AphrontQueryConnectionException' => 'AphrontQueryException', 1119 1120 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 1120 1121 'AphrontQueryCountException' => 'AphrontQueryException', 1122 + 'AphrontQueryDeadlockException' => 'AphrontQueryRecoverableException', 1121 1123 'AphrontQueryDuplicateKeyException' => 'AphrontQueryException', 1122 1124 'AphrontQueryObjectMissingException' => 'AphrontQueryException', 1123 1125 'AphrontQueryParameterException' => 'AphrontQueryException', ··· 1207 1209 'ConduitAPI_slowvote_info_Method' => 'ConduitAPIMethod', 1208 1210 'ConduitAPI_user_Method' => 'ConduitAPIMethod', 1209 1211 'ConduitAPI_user_addstatus_Method' => 'ConduitAPI_user_Method', 1210 - 'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method', 1211 1212 'ConduitAPI_user_find_Method' => 'ConduitAPI_user_Method', 1212 1213 'ConduitAPI_user_info_Method' => 'ConduitAPI_user_Method', 1214 + 'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method', 1213 1215 'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method', 1214 1216 'DarkConsoleConfigPlugin' => 'DarkConsolePlugin', 1215 1217 'DarkConsoleController' => 'PhabricatorController',
+56 -6
src/storage/connection/base/AphrontDatabaseConnection.php
··· 22 22 */ 23 23 abstract class AphrontDatabaseConnection { 24 24 25 - private static $transactionStates = array(); 25 + private $transactionState; 26 26 27 27 abstract public function getInsertID(); 28 28 abstract public function getAffectedRows(); 29 29 abstract public function selectAllResults(); 30 30 abstract public function executeRawQuery($raw_query); 31 - abstract protected function getTransactionKey(); 32 31 33 32 abstract public function escapeString($string); 34 33 abstract public function escapeColumnName($string); ··· 133 132 * @task xaction 134 133 */ 135 134 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(); 139 137 } 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(); 141 191 } 142 192 143 193 }
-8
src/storage/connection/isolated/AphrontIsolatedDatabaseConnection.php
··· 25 25 private $configuration; 26 26 private static $nextInsertID; 27 27 private $insertID; 28 - private static $nextTransactionKey = 1; 29 - private $transactionKey; 30 28 31 29 private $transcript = array(); 32 30 ··· 38 36 // collisions and make them distinctive. 39 37 self::$nextInsertID = 55555000000 + mt_rand(0, 1000); 40 38 } 41 - 42 - $this->transactionKey = 'iso-xaction-'.(self::$nextTransactionKey++); 43 39 } 44 40 45 41 public function escapeString($string) { ··· 68 64 69 65 public function getAffectedRows() { 70 66 return $this->affectedRows; 71 - } 72 - 73 - protected function getTransactionKey() { 74 - return $this->transactionKey; 75 67 } 76 68 77 69 public function selectAllResults() {
+1 -1
src/storage/connection/mysql/base/AphrontMySQLDatabaseConnectionBase.php
··· 225 225 throw new AphrontQueryConnectionLostException($exmsg); 226 226 case 1213: // Deadlock 227 227 case 1205: // Lock wait timeout exceeded 228 - throw new AphrontQueryRecoverableException($exmsg); 228 + throw new AphrontQueryDeadlockException($exmsg); 229 229 case 1062: // Duplicate Key 230 230 // NOTE: In some versions of MySQL we get a key name back here, but 231 231 // older versions just give us a key index ("key 2") so it's not
+1 -1
src/storage/connection/mysql/base/__init__.php
··· 12 12 phutil_require_module('phabricator', 'storage/exception/accessdenied'); 13 13 phutil_require_module('phabricator', 'storage/exception/base'); 14 14 phutil_require_module('phabricator', 'storage/exception/connectionlost'); 15 + phutil_require_module('phabricator', 'storage/exception/deadlock'); 15 16 phutil_require_module('phabricator', 'storage/exception/duplicatekey'); 16 - phutil_require_module('phabricator', 'storage/exception/recoverable'); 17 17 phutil_require_module('phabricator', 'storage/exception/schema'); 18 18 19 19 phutil_require_module('phutil', 'error');
-4
src/storage/connection/mysql/mysql/AphrontMySQLDatabaseConnection.php
··· 34 34 return mysql_affected_rows($this->requireConnection()); 35 35 } 36 36 37 - protected function getTransactionKey() { 38 - return (int)$this->requireConnection(); 39 - } 40 - 41 37 protected function connect() { 42 38 if (!function_exists('mysql_connect')) { 43 39 // We have to '@' the actual call since it can spew all sorts of silly
-4
src/storage/connection/mysql/mysqli/AphrontMySQLiDatabaseConnection.php
··· 36 36 return $this->requireConnection()->affected_rows; 37 37 } 38 38 39 - protected function getTransactionKey() { 40 - return spl_object_hash($this->requireConnection()); 41 - } 42 - 43 39 protected function connect() { 44 40 if (!class_exists('mysqli', false)) { 45 41 throw new Exception(
+23
src/storage/exception/deadlock/AphrontQueryDeadlockException.php
··· 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 { }
+12
src/storage/exception/deadlock/__init__.php
··· 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');
+66 -5
src/storage/lisk/dao/LiskDAO.php
··· 542 542 $connection = $this->establishConnection('r'); 543 543 544 544 $lock_clause = ''; 545 - /* 546 - 547 - TODO: Restore this? 548 - 549 545 if ($connection->isReadLocking()) { 550 546 $lock_clause = 'FOR UPDATE'; 551 547 } else if ($connection->isWriteLocking()) { 552 548 $lock_clause = 'LOCK IN SHARE MODE'; 553 549 } 554 - */ 555 550 556 551 $args = func_get_args(); 557 552 $args = array_slice($args, 2); ··· 1342 1337 } 1343 1338 1344 1339 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 + 1345 1405 /* -( Isolation )---------------------------------------------------------- */ 1406 + 1346 1407 1347 1408 /** 1348 1409 * @task isolate
+47 -1
src/storage/transaction/AphrontDatabaseTransactionState.php
··· 23 23 */ 24 24 final class AphrontDatabaseTransactionState { 25 25 26 - private $depth; 26 + private $depth = 0; 27 + private $readLockLevel = 0; 28 + private $writeLockLevel = 0; 27 29 28 30 public function getDepth() { 29 31 return $this->depth; ··· 46 48 return 'Aphront_Savepoint_'.$this->depth; 47 49 } 48 50 51 + public function beginReadLocking() { 52 + $this->readLockLevel++; 53 + return $this; 54 + } 55 + 56 + public function endReadLocking() { 57 + if ($this->readLockLevel == 0) { 58 + throw new Exception("Too many calls to endReadLocking()!"); 59 + } 60 + $this->readLockLevel--; 61 + return $this; 62 + } 63 + 64 + public function isReadLocking() { 65 + return ($this->readLockLevel > 0); 66 + } 67 + 68 + public function beginWriteLocking() { 69 + $this->writeLockLevel++; 70 + return $this; 71 + } 72 + 73 + public function endWriteLocking() { 74 + if ($this->writeLockLevel == 0) { 75 + throw new Exception("Too many calls to endWriteLocking()!"); 76 + } 77 + $this->writeLockLevel--; 78 + return $this; 79 + } 80 + 81 + public function isWriteLocking() { 82 + return ($this->writeLockLevel > 0); 83 + } 84 + 49 85 public function __destruct() { 50 86 if ($this->depth) { 51 87 throw new Exception( 52 88 'Process exited with an open transaction! The transaction will be '. 53 89 'implicitly rolled back. Calls to openTransaction() must always be '. 54 90 'paired with a call to saveTransaction() or killTransaction().'); 91 + } 92 + if ($this->readLockLevel) { 93 + throw new Exception( 94 + 'Process exited with an open read lock! Call to beginReadLocking() '. 95 + 'must always be paired with a call to endReadLocking().'); 96 + } 97 + if ($this->writeLockLevel) { 98 + throw new Exception( 99 + 'Process exited with an open write lock! Call to beginWriteLocking() '. 100 + 'must always be paired with a call to endWriteLocking().'); 55 101 } 56 102 } 57 103