PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/filebackend/lockmanager/DBLockManager.php

https://gitlab.com/link233/bootmw
PHP | 433 lines | 232 code | 41 blank | 160 comment | 27 complexity | 89f9d95f76f8b8ce5590ae0b3fa95553 MD5 | raw file
  1. <?php
  2. /**
  3. * Version of LockManager based on using DB table locks.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup LockManager
  22. */
  23. /**
  24. * Version of LockManager based on using named/row DB locks.
  25. *
  26. * This is meant for multi-wiki systems that may share files.
  27. *
  28. * All lock requests for a resource, identified by a hash string, will map
  29. * to one bucket. Each bucket maps to one or several peer DBs, each on their
  30. * own server, all having the filelocks.sql tables (with row-level locking).
  31. * A majority of peer DBs must agree for a lock to be acquired.
  32. *
  33. * Caching is used to avoid hitting servers that are down.
  34. *
  35. * @ingroup LockManager
  36. * @since 1.19
  37. */
  38. abstract class DBLockManager extends QuorumLockManager {
  39. /** @var array Map of DB names to server config */
  40. protected $dbServers; // (DB name => server config array)
  41. /** @var BagOStuff */
  42. protected $statusCache;
  43. protected $lockExpiry; // integer number of seconds
  44. protected $safeDelay; // integer number of seconds
  45. protected $session = 0; // random integer
  46. /** @var array Map Database connections (DB name => Database) */
  47. protected $conns = [];
  48. /**
  49. * Construct a new instance from configuration.
  50. *
  51. * @param array $config Parameters include:
  52. * - dbServers : Associative array of DB names to server configuration.
  53. * Configuration is an associative array that includes:
  54. * - host : DB server name
  55. * - dbname : DB name
  56. * - type : DB type (mysql,postgres,...)
  57. * - user : DB user
  58. * - password : DB user password
  59. * - tablePrefix : DB table prefix
  60. * - flags : DB flags (see DatabaseBase)
  61. * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
  62. * each having an odd-numbered list of DB names (peers) as values.
  63. * Any DB named 'localDBMaster' will automatically use the DB master
  64. * settings for this wiki (without the need for a dbServers entry).
  65. * Only use 'localDBMaster' if the domain is a valid wiki ID.
  66. * - lockExpiry : Lock timeout (seconds) for dropped connections. [optional]
  67. * This tells the DB server how long to wait before assuming
  68. * connection failure and releasing all the locks for a session.
  69. */
  70. public function __construct( array $config ) {
  71. parent::__construct( $config );
  72. $this->dbServers = isset( $config['dbServers'] )
  73. ? $config['dbServers']
  74. : []; // likely just using 'localDBMaster'
  75. // Sanitize srvsByBucket config to prevent PHP errors
  76. $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
  77. $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
  78. if ( isset( $config['lockExpiry'] ) ) {
  79. $this->lockExpiry = $config['lockExpiry'];
  80. } else {
  81. $met = ini_get( 'max_execution_time' );
  82. $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
  83. }
  84. $this->safeDelay = ( $this->lockExpiry <= 0 )
  85. ? 60 // pick a safe-ish number to match DB timeout default
  86. : $this->lockExpiry; // cover worst case
  87. foreach ( $this->srvsByBucket as $bucket ) {
  88. if ( count( $bucket ) > 1 ) { // multiple peers
  89. // Tracks peers that couldn't be queried recently to avoid lengthy
  90. // connection timeouts. This is useless if each bucket has one peer.
  91. $this->statusCache = ObjectCache::getLocalServerInstance();
  92. break;
  93. }
  94. }
  95. $this->session = wfRandomString( 31 );
  96. }
  97. // @todo change this code to work in one batch
  98. protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
  99. $status = Status::newGood();
  100. foreach ( $pathsByType as $type => $paths ) {
  101. $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
  102. }
  103. return $status;
  104. }
  105. protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
  106. return Status::newGood();
  107. }
  108. /**
  109. * @see QuorumLockManager::isServerUp()
  110. * @param string $lockSrv
  111. * @return bool
  112. */
  113. protected function isServerUp( $lockSrv ) {
  114. if ( !$this->cacheCheckFailures( $lockSrv ) ) {
  115. return false; // recent failure to connect
  116. }
  117. try {
  118. $this->getConnection( $lockSrv );
  119. } catch ( DBError $e ) {
  120. $this->cacheRecordFailure( $lockSrv );
  121. return false; // failed to connect
  122. }
  123. return true;
  124. }
  125. /**
  126. * Get (or reuse) a connection to a lock DB
  127. *
  128. * @param string $lockDb
  129. * @return IDatabase
  130. * @throws DBError
  131. */
  132. protected function getConnection( $lockDb ) {
  133. if ( !isset( $this->conns[$lockDb] ) ) {
  134. $db = null;
  135. if ( $lockDb === 'localDBMaster' ) {
  136. $lb = wfGetLBFactory()->getMainLB( $this->domain );
  137. $db = $lb->getConnection( DB_MASTER, [], $this->domain );
  138. } elseif ( isset( $this->dbServers[$lockDb] ) ) {
  139. $config = $this->dbServers[$lockDb];
  140. $db = DatabaseBase::factory( $config['type'], $config );
  141. }
  142. if ( !$db ) {
  143. return null; // config error?
  144. }
  145. $this->conns[$lockDb] = $db;
  146. $this->conns[$lockDb]->clearFlag( DBO_TRX );
  147. # If the connection drops, try to avoid letting the DB rollback
  148. # and release the locks before the file operations are finished.
  149. # This won't handle the case of DB server restarts however.
  150. $options = [];
  151. if ( $this->lockExpiry > 0 ) {
  152. $options['connTimeout'] = $this->lockExpiry;
  153. }
  154. $this->conns[$lockDb]->setSessionOptions( $options );
  155. $this->initConnection( $lockDb, $this->conns[$lockDb] );
  156. }
  157. if ( !$this->conns[$lockDb]->trxLevel() ) {
  158. $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
  159. }
  160. return $this->conns[$lockDb];
  161. }
  162. /**
  163. * Do additional initialization for new lock DB connection
  164. *
  165. * @param string $lockDb
  166. * @param IDatabase $db
  167. * @throws DBError
  168. */
  169. protected function initConnection( $lockDb, IDatabase $db ) {
  170. }
  171. /**
  172. * Checks if the DB has not recently had connection/query errors.
  173. * This just avoids wasting time on doomed connection attempts.
  174. *
  175. * @param string $lockDb
  176. * @return bool
  177. */
  178. protected function cacheCheckFailures( $lockDb ) {
  179. return ( $this->statusCache && $this->safeDelay > 0 )
  180. ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
  181. : true;
  182. }
  183. /**
  184. * Log a lock request failure to the cache
  185. *
  186. * @param string $lockDb
  187. * @return bool Success
  188. */
  189. protected function cacheRecordFailure( $lockDb ) {
  190. return ( $this->statusCache && $this->safeDelay > 0 )
  191. ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
  192. : true;
  193. }
  194. /**
  195. * Get a cache key for recent query misses for a DB
  196. *
  197. * @param string $lockDb
  198. * @return string
  199. */
  200. protected function getMissKey( $lockDb ) {
  201. $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
  202. return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
  203. }
  204. /**
  205. * Make sure remaining locks get cleared for sanity
  206. */
  207. function __destruct() {
  208. $this->releaseAllLocks();
  209. foreach ( $this->conns as $db ) {
  210. $db->close();
  211. }
  212. }
  213. }
  214. /**
  215. * MySQL version of DBLockManager that supports shared locks.
  216. * All locks are non-blocking, which avoids deadlocks.
  217. *
  218. * @ingroup LockManager
  219. */
  220. class MySqlLockManager extends DBLockManager {
  221. /** @var array Mapping of lock types to the type actually used */
  222. protected $lockTypeMap = [
  223. self::LOCK_SH => self::LOCK_SH,
  224. self::LOCK_UW => self::LOCK_SH,
  225. self::LOCK_EX => self::LOCK_EX
  226. ];
  227. /**
  228. * @param string $lockDb
  229. * @param IDatabase $db
  230. */
  231. protected function initConnection( $lockDb, IDatabase $db ) {
  232. # Let this transaction see lock rows from other transactions
  233. $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
  234. }
  235. /**
  236. * Get a connection to a lock DB and acquire locks on $paths.
  237. * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
  238. *
  239. * @see DBLockManager::getLocksOnServer()
  240. * @param string $lockSrv
  241. * @param array $paths
  242. * @param string $type
  243. * @return Status
  244. */
  245. protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
  246. $status = Status::newGood();
  247. $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
  248. $keys = []; // list of hash keys for the paths
  249. $data = []; // list of rows to insert
  250. $checkEXKeys = []; // list of hash keys that this has no EX lock on
  251. # Build up values for INSERT clause
  252. foreach ( $paths as $path ) {
  253. $key = $this->sha1Base36Absolute( $path );
  254. $keys[] = $key;
  255. $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
  256. if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
  257. $checkEXKeys[] = $key;
  258. }
  259. }
  260. # Block new writers (both EX and SH locks leave entries here)...
  261. $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
  262. # Actually do the locking queries...
  263. if ( $type == self::LOCK_SH ) { // reader locks
  264. $blocked = false;
  265. # Bail if there are any existing writers...
  266. if ( count( $checkEXKeys ) ) {
  267. $blocked = $db->selectField( 'filelocks_exclusive', '1',
  268. [ 'fle_key' => $checkEXKeys ],
  269. __METHOD__
  270. );
  271. }
  272. # Other prospective writers that haven't yet updated filelocks_exclusive
  273. # will recheck filelocks_shared after doing so and bail due to this entry.
  274. } else { // writer locks
  275. $encSession = $db->addQuotes( $this->session );
  276. # Bail if there are any existing writers...
  277. # This may detect readers, but the safe check for them is below.
  278. # Note: if two writers come at the same time, both bail :)
  279. $blocked = $db->selectField( 'filelocks_shared', '1',
  280. [ 'fls_key' => $keys, "fls_session != $encSession" ],
  281. __METHOD__
  282. );
  283. if ( !$blocked ) {
  284. # Build up values for INSERT clause
  285. $data = [];
  286. foreach ( $keys as $key ) {
  287. $data[] = [ 'fle_key' => $key ];
  288. }
  289. # Block new readers/writers...
  290. $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
  291. # Bail if there are any existing readers...
  292. $blocked = $db->selectField( 'filelocks_shared', '1',
  293. [ 'fls_key' => $keys, "fls_session != $encSession" ],
  294. __METHOD__
  295. );
  296. }
  297. }
  298. if ( $blocked ) {
  299. foreach ( $paths as $path ) {
  300. $status->fatal( 'lockmanager-fail-acquirelock', $path );
  301. }
  302. }
  303. return $status;
  304. }
  305. /**
  306. * @see QuorumLockManager::releaseAllLocks()
  307. * @return Status
  308. */
  309. protected function releaseAllLocks() {
  310. $status = Status::newGood();
  311. foreach ( $this->conns as $lockDb => $db ) {
  312. if ( $db->trxLevel() ) { // in transaction
  313. try {
  314. $db->rollback( __METHOD__ ); // finish transaction and kill any rows
  315. } catch ( DBError $e ) {
  316. $status->fatal( 'lockmanager-fail-db-release', $lockDb );
  317. }
  318. }
  319. }
  320. return $status;
  321. }
  322. }
  323. /**
  324. * PostgreSQL version of DBLockManager that supports shared locks.
  325. * All locks are non-blocking, which avoids deadlocks.
  326. *
  327. * @ingroup LockManager
  328. */
  329. class PostgreSqlLockManager extends DBLockManager {
  330. /** @var array Mapping of lock types to the type actually used */
  331. protected $lockTypeMap = [
  332. self::LOCK_SH => self::LOCK_SH,
  333. self::LOCK_UW => self::LOCK_SH,
  334. self::LOCK_EX => self::LOCK_EX
  335. ];
  336. protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
  337. $status = Status::newGood();
  338. if ( !count( $paths ) ) {
  339. return $status; // nothing to lock
  340. }
  341. $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
  342. $bigints = array_unique( array_map(
  343. function ( $key ) {
  344. return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
  345. },
  346. array_map( [ $this, 'sha1Base16Absolute' ], $paths )
  347. ) );
  348. // Try to acquire all the locks...
  349. $fields = [];
  350. foreach ( $bigints as $bigint ) {
  351. $fields[] = ( $type == self::LOCK_SH )
  352. ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
  353. : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
  354. }
  355. $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
  356. $row = $res->fetchRow();
  357. if ( in_array( 'f', $row ) ) {
  358. // Release any acquired locks if some could not be acquired...
  359. $fields = [];
  360. foreach ( $row as $kbigint => $ok ) {
  361. if ( $ok === 't' ) { // locked
  362. $bigint = substr( $kbigint, 1 ); // strip off the "K"
  363. $fields[] = ( $type == self::LOCK_SH )
  364. ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
  365. : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
  366. }
  367. }
  368. if ( count( $fields ) ) {
  369. $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
  370. }
  371. foreach ( $paths as $path ) {
  372. $status->fatal( 'lockmanager-fail-acquirelock', $path );
  373. }
  374. }
  375. return $status;
  376. }
  377. /**
  378. * @see QuorumLockManager::releaseAllLocks()
  379. * @return Status
  380. */
  381. protected function releaseAllLocks() {
  382. $status = Status::newGood();
  383. foreach ( $this->conns as $lockDb => $db ) {
  384. try {
  385. $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
  386. } catch ( DBError $e ) {
  387. $status->fatal( 'lockmanager-fail-db-release', $lockDb );
  388. }
  389. }
  390. return $status;
  391. }
  392. }