MediaWiki  master
DBLockManager.php
Go to the documentation of this file.
1 <?php
39 abstract class DBLockManager extends QuorumLockManager {
41  protected $dbServers; // (DB name => server config array)
43  protected $statusCache;
44 
45  protected $lockExpiry; // integer number of seconds
46  protected $safeDelay; // integer number of seconds
47 
48  protected $session = 0; // random integer
50  protected $conns = [];
51 
74  public function __construct( array $config ) {
75  parent::__construct( $config );
76 
77  $this->dbServers = isset( $config['dbServers'] )
78  ? $config['dbServers']
79  : []; // likely just using 'localDBMaster'
80  // Sanitize srvsByBucket config to prevent PHP errors
81  $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
82  $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
83 
84  if ( isset( $config['lockExpiry'] ) ) {
85  $this->lockExpiry = $config['lockExpiry'];
86  } else {
87  $met = ini_get( 'max_execution_time' );
88  $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
89  }
90  $this->safeDelay = ( $this->lockExpiry <= 0 )
91  ? 60 // pick a safe-ish number to match DB timeout default
92  : $this->lockExpiry; // cover worst case
93 
94  foreach ( $this->srvsByBucket as $bucket ) {
95  if ( count( $bucket ) > 1 ) { // multiple peers
96  // Tracks peers that couldn't be queried recently to avoid lengthy
97  // connection timeouts. This is useless if each bucket has one peer.
98  $this->statusCache = ObjectCache::getLocalServerInstance();
99  break;
100  }
101  }
102 
103  $this->session = wfRandomString( 31 );
104  }
105 
106  // @todo change this code to work in one batch
107  protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
109  foreach ( $pathsByType as $type => $paths ) {
110  $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
111  }
112 
113  return $status;
114  }
115 
116  abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
117 
118  protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
119  return Status::newGood();
120  }
121 
127  protected function isServerUp( $lockSrv ) {
128  if ( !$this->cacheCheckFailures( $lockSrv ) ) {
129  return false; // recent failure to connect
130  }
131  try {
132  $this->getConnection( $lockSrv );
133  } catch ( DBError $e ) {
134  $this->cacheRecordFailure( $lockSrv );
135 
136  return false; // failed to connect
137  }
138 
139  return true;
140  }
141 
149  protected function getConnection( $lockDb ) {
150  if ( !isset( $this->conns[$lockDb] ) ) {
151  $db = null;
152  if ( $lockDb === 'localDBMaster' ) {
153  $lb = wfGetLBFactory()->getMainLB( $this->domain );
154  $db = $lb->getConnection( DB_MASTER, [], $this->domain );
155  } elseif ( isset( $this->dbServers[$lockDb] ) ) {
156  $config = $this->dbServers[$lockDb];
157  $db = DatabaseBase::factory( $config['type'], $config );
158  }
159  if ( !$db ) {
160  return null; // config error?
161  }
162  $this->conns[$lockDb] = $db;
163  $this->conns[$lockDb]->clearFlag( DBO_TRX );
164  # If the connection drops, try to avoid letting the DB rollback
165  # and release the locks before the file operations are finished.
166  # This won't handle the case of DB server restarts however.
167  $options = [];
168  if ( $this->lockExpiry > 0 ) {
169  $options['connTimeout'] = $this->lockExpiry;
170  }
171  $this->conns[$lockDb]->setSessionOptions( $options );
172  $this->initConnection( $lockDb, $this->conns[$lockDb] );
173  }
174  if ( !$this->conns[$lockDb]->trxLevel() ) {
175  $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
176  }
177 
178  return $this->conns[$lockDb];
179  }
180 
188  protected function initConnection( $lockDb, IDatabase $db ) {
189  }
190 
198  protected function cacheCheckFailures( $lockDb ) {
199  return ( $this->statusCache && $this->safeDelay > 0 )
200  ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
201  : true;
202  }
203 
210  protected function cacheRecordFailure( $lockDb ) {
211  return ( $this->statusCache && $this->safeDelay > 0 )
212  ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
213  : true;
214  }
215 
222  protected function getMissKey( $lockDb ) {
223  $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
224  return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
225  }
226 
230  function __destruct() {
231  $this->releaseAllLocks();
232  foreach ( $this->conns as $db ) {
233  $db->close();
234  }
235  }
236 }
237 
246  protected $lockTypeMap = [
247  self::LOCK_SH => self::LOCK_SH,
248  self::LOCK_UW => self::LOCK_SH,
249  self::LOCK_EX => self::LOCK_EX
250  ];
251 
256  protected function initConnection( $lockDb, IDatabase $db ) {
257  # Let this transaction see lock rows from other transactions
258  $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
259  }
260 
271  protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
273 
274  $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
275 
276  $keys = []; // list of hash keys for the paths
277  $data = []; // list of rows to insert
278  $checkEXKeys = []; // list of hash keys that this has no EX lock on
279  # Build up values for INSERT clause
280  foreach ( $paths as $path ) {
281  $key = $this->sha1Base36Absolute( $path );
282  $keys[] = $key;
283  $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
284  if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
285  $checkEXKeys[] = $key;
286  }
287  }
288 
289  # Block new writers (both EX and SH locks leave entries here)...
290  $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
291  # Actually do the locking queries...
292  if ( $type == self::LOCK_SH ) { // reader locks
293  $blocked = false;
294  # Bail if there are any existing writers...
295  if ( count( $checkEXKeys ) ) {
296  $blocked = $db->selectField( 'filelocks_exclusive', '1',
297  [ 'fle_key' => $checkEXKeys ],
298  __METHOD__
299  );
300  }
301  # Other prospective writers that haven't yet updated filelocks_exclusive
302  # will recheck filelocks_shared after doing so and bail due to this entry.
303  } else { // writer locks
304  $encSession = $db->addQuotes( $this->session );
305  # Bail if there are any existing writers...
306  # This may detect readers, but the safe check for them is below.
307  # Note: if two writers come at the same time, both bail :)
308  $blocked = $db->selectField( 'filelocks_shared', '1',
309  [ 'fls_key' => $keys, "fls_session != $encSession" ],
310  __METHOD__
311  );
312  if ( !$blocked ) {
313  # Build up values for INSERT clause
314  $data = [];
315  foreach ( $keys as $key ) {
316  $data[] = [ 'fle_key' => $key ];
317  }
318  # Block new readers/writers...
319  $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
320  # Bail if there are any existing readers...
321  $blocked = $db->selectField( 'filelocks_shared', '1',
322  [ 'fls_key' => $keys, "fls_session != $encSession" ],
323  __METHOD__
324  );
325  }
326  }
327 
328  if ( $blocked ) {
329  foreach ( $paths as $path ) {
330  $status->fatal( 'lockmanager-fail-acquirelock', $path );
331  }
332  }
333 
334  return $status;
335  }
336 
341  protected function releaseAllLocks() {
343 
344  foreach ( $this->conns as $lockDb => $db ) {
345  if ( $db->trxLevel() ) { // in transaction
346  try {
347  $db->rollback( __METHOD__ ); // finish transaction and kill any rows
348  } catch ( DBError $e ) {
349  $status->fatal( 'lockmanager-fail-db-release', $lockDb );
350  }
351  }
352  }
353 
354  return $status;
355  }
356 }
357 
366  protected $lockTypeMap = [
367  self::LOCK_SH => self::LOCK_SH,
368  self::LOCK_UW => self::LOCK_SH,
369  self::LOCK_EX => self::LOCK_EX
370  ];
371 
372  protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
374  if ( !count( $paths ) ) {
375  return $status; // nothing to lock
376  }
377 
378  $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
379  $bigints = array_unique( array_map(
380  function ( $key ) {
381  return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
382  },
383  array_map( [ $this, 'sha1Base16Absolute' ], $paths )
384  ) );
385 
386  // Try to acquire all the locks...
387  $fields = [];
388  foreach ( $bigints as $bigint ) {
389  $fields[] = ( $type == self::LOCK_SH )
390  ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
391  : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
392  }
393  $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
394  $row = $res->fetchRow();
395 
396  if ( in_array( 'f', $row ) ) {
397  // Release any acquired locks if some could not be acquired...
398  $fields = [];
399  foreach ( $row as $kbigint => $ok ) {
400  if ( $ok === 't' ) { // locked
401  $bigint = substr( $kbigint, 1 ); // strip off the "K"
402  $fields[] = ( $type == self::LOCK_SH )
403  ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
404  : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
405  }
406  }
407  if ( count( $fields ) ) {
408  $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
409  }
410  foreach ( $paths as $path ) {
411  $status->fatal( 'lockmanager-fail-acquirelock', $path );
412  }
413  }
414 
415  return $status;
416  }
417 
422  protected function releaseAllLocks() {
424 
425  foreach ( $this->conns as $lockDb => $db ) {
426  try {
427  $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
428  } catch ( DBError $e ) {
429  $status->fatal( 'lockmanager-fail-db-release', $lockDb );
430  }
431  }
432 
433  return $status;
434  }
435 }
initConnection($lockDb, IDatabase $db)
__destruct()
Make sure remaining locks get cleared for sanity.
sha1Base36Absolute($path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
array[] $dbServers
Map of DB names to server config.
cacheRecordFailure($lockDb)
Log a lock request failure to the cache.
freeLocksOnServer($lockSrv, array $pathsByType)
array $lockTypeMap
Mapping of lock types to the type actually used.
Database error base class.
the array() calling protocol came about after MediaWiki 1.4rc1.
doGetLocksOnServer($lockSrv, array $paths, $type)
Get a connection to a lock DB and acquire locks on $paths.
releaseAllLocks()
Release all locks that this session is holding.
cacheCheckFailures($lockDb)
Checks if the DB has not recently had connection/query errors.
doGetLocksOnServer($lockSrv, array $paths, $type)
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1980
query($sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Version of LockManager based on using named/row DB locks.
const DBO_TRX
Definition: Defines.php:33
getLocksOnServer($lockSrv, array $pathsByType)
wfRandomString($length=32)
Get a random string containing a number of pseudo-random hex characters.
array $lockTypeMap
Mapping of lock types to the type actually used.
BagOStuff $statusCache
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1020
isServerUp($lockSrv)
$res
Definition: database.txt:21
static factory($dbType, $p=[])
Given a DB type, construct the name of the appropriate child class of DatabaseBase.
Definition: Database.php:584
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
doGetLocksOnServer($lockSrv, array $paths, $type)
MySQL version of DBLockManager that supports shared locks.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
wfGetLBFactory()
Get the load balancer factory object.
static getLocalServerInstance($fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
__construct(array $config)
Construct a new instance from configuration.
getConnection($lockDb)
Get (or reuse) a connection to a lock DB.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1020
PostgreSQL version of DBLockManager that supports shared locks.
getMissKey($lockDb)
Get a cache key for recent query misses for a DB.
const DB_MASTER
Definition: Defines.php:47
initConnection($lockDb, IDatabase $db)
Do additional initialization for new lock DB connection.
IDatabase[] $conns
Map Database connections (DB name => Database)
Version of LockManager that uses a quorum from peer servers for locks.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2376
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
Basic database interface for live and lazy-loaded DB handles.
Definition: IDatabase.php:35