MediaWiki  1.27.2
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  protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
117  return Status::newGood();
118  }
119 
125  protected function isServerUp( $lockSrv ) {
126  if ( !$this->cacheCheckFailures( $lockSrv ) ) {
127  return false; // recent failure to connect
128  }
129  try {
130  $this->getConnection( $lockSrv );
131  } catch ( DBError $e ) {
132  $this->cacheRecordFailure( $lockSrv );
133 
134  return false; // failed to connect
135  }
136 
137  return true;
138  }
139 
147  protected function getConnection( $lockDb ) {
148  if ( !isset( $this->conns[$lockDb] ) ) {
149  $db = null;
150  if ( $lockDb === 'localDBMaster' ) {
151  $lb = wfGetLBFactory()->getMainLB( $this->domain );
152  $db = $lb->getConnection( DB_MASTER, [], $this->domain );
153  } elseif ( isset( $this->dbServers[$lockDb] ) ) {
154  $config = $this->dbServers[$lockDb];
155  $db = DatabaseBase::factory( $config['type'], $config );
156  }
157  if ( !$db ) {
158  return null; // config error?
159  }
160  $this->conns[$lockDb] = $db;
161  $this->conns[$lockDb]->clearFlag( DBO_TRX );
162  # If the connection drops, try to avoid letting the DB rollback
163  # and release the locks before the file operations are finished.
164  # This won't handle the case of DB server restarts however.
165  $options = [];
166  if ( $this->lockExpiry > 0 ) {
167  $options['connTimeout'] = $this->lockExpiry;
168  }
169  $this->conns[$lockDb]->setSessionOptions( $options );
170  $this->initConnection( $lockDb, $this->conns[$lockDb] );
171  }
172  if ( !$this->conns[$lockDb]->trxLevel() ) {
173  $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
174  }
175 
176  return $this->conns[$lockDb];
177  }
178 
186  protected function initConnection( $lockDb, IDatabase $db ) {
187  }
188 
196  protected function cacheCheckFailures( $lockDb ) {
197  return ( $this->statusCache && $this->safeDelay > 0 )
198  ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
199  : true;
200  }
201 
208  protected function cacheRecordFailure( $lockDb ) {
209  return ( $this->statusCache && $this->safeDelay > 0 )
210  ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
211  : true;
212  }
213 
220  protected function getMissKey( $lockDb ) {
221  $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
222  return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
223  }
224 
228  function __destruct() {
229  $this->releaseAllLocks();
230  foreach ( $this->conns as $db ) {
231  $db->close();
232  }
233  }
234 }
235 
244  protected $lockTypeMap = [
245  self::LOCK_SH => self::LOCK_SH,
246  self::LOCK_UW => self::LOCK_SH,
247  self::LOCK_EX => self::LOCK_EX
248  ];
249 
254  protected function initConnection( $lockDb, IDatabase $db ) {
255  # Let this transaction see lock rows from other transactions
256  $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
257  }
258 
269  protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
271 
272  $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
273 
274  $keys = []; // list of hash keys for the paths
275  $data = []; // list of rows to insert
276  $checkEXKeys = []; // list of hash keys that this has no EX lock on
277  # Build up values for INSERT clause
278  foreach ( $paths as $path ) {
279  $key = $this->sha1Base36Absolute( $path );
280  $keys[] = $key;
281  $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
282  if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
283  $checkEXKeys[] = $key;
284  }
285  }
286 
287  # Block new writers (both EX and SH locks leave entries here)...
288  $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
289  # Actually do the locking queries...
290  if ( $type == self::LOCK_SH ) { // reader locks
291  $blocked = false;
292  # Bail if there are any existing writers...
293  if ( count( $checkEXKeys ) ) {
294  $blocked = $db->selectField( 'filelocks_exclusive', '1',
295  [ 'fle_key' => $checkEXKeys ],
296  __METHOD__
297  );
298  }
299  # Other prospective writers that haven't yet updated filelocks_exclusive
300  # will recheck filelocks_shared after doing so and bail due to this entry.
301  } else { // writer locks
302  $encSession = $db->addQuotes( $this->session );
303  # Bail if there are any existing writers...
304  # This may detect readers, but the safe check for them is below.
305  # Note: if two writers come at the same time, both bail :)
306  $blocked = $db->selectField( 'filelocks_shared', '1',
307  [ 'fls_key' => $keys, "fls_session != $encSession" ],
308  __METHOD__
309  );
310  if ( !$blocked ) {
311  # Build up values for INSERT clause
312  $data = [];
313  foreach ( $keys as $key ) {
314  $data[] = [ 'fle_key' => $key ];
315  }
316  # Block new readers/writers...
317  $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
318  # Bail if there are any existing readers...
319  $blocked = $db->selectField( 'filelocks_shared', '1',
320  [ 'fls_key' => $keys, "fls_session != $encSession" ],
321  __METHOD__
322  );
323  }
324  }
325 
326  if ( $blocked ) {
327  foreach ( $paths as $path ) {
328  $status->fatal( 'lockmanager-fail-acquirelock', $path );
329  }
330  }
331 
332  return $status;
333  }
334 
339  protected function releaseAllLocks() {
341 
342  foreach ( $this->conns as $lockDb => $db ) {
343  if ( $db->trxLevel() ) { // in transaction
344  try {
345  $db->rollback( __METHOD__ ); // finish transaction and kill any rows
346  } catch ( DBError $e ) {
347  $status->fatal( 'lockmanager-fail-db-release', $lockDb );
348  }
349  }
350  }
351 
352  return $status;
353  }
354 }
355 
364  protected $lockTypeMap = [
365  self::LOCK_SH => self::LOCK_SH,
366  self::LOCK_UW => self::LOCK_SH,
367  self::LOCK_EX => self::LOCK_EX
368  ];
369 
370  protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
372  if ( !count( $paths ) ) {
373  return $status; // nothing to lock
374  }
375 
376  $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
377  $bigints = array_unique( array_map(
378  function ( $key ) {
379  return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
380  },
381  array_map( [ $this, 'sha1Base16Absolute' ], $paths )
382  ) );
383 
384  // Try to acquire all the locks...
385  $fields = [];
386  foreach ( $bigints as $bigint ) {
387  $fields[] = ( $type == self::LOCK_SH )
388  ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
389  : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
390  }
391  $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
392  $row = $res->fetchRow();
393 
394  if ( in_array( 'f', $row ) ) {
395  // Release any acquired locks if some could not be acquired...
396  $fields = [];
397  foreach ( $row as $kbigint => $ok ) {
398  if ( $ok === 't' ) { // locked
399  $bigint = substr( $kbigint, 1 ); // strip off the "K"
400  $fields[] = ( $type == self::LOCK_SH )
401  ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
402  : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
403  }
404  }
405  if ( count( $fields ) ) {
406  $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
407  }
408  foreach ( $paths as $path ) {
409  $status->fatal( 'lockmanager-fail-acquirelock', $path );
410  }
411  }
412 
413  return $status;
414  }
415 
420  protected function releaseAllLocks() {
422 
423  foreach ( $this->conns as $lockDb => $db ) {
424  try {
425  $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
426  } catch ( DBError $e ) {
427  $status->fatal( 'lockmanager-fail-db-release', $lockDb );
428  }
429  }
430 
431  return $status;
432  }
433 }
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.
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.
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
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:1932
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 $dbServers
Map of DB names to server config.
array $lockTypeMap
Mapping of lock types to the type actually used.
array $conns
Map Database connections (DB name => Database)
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:1004
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:580
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
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:1004
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.
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:2338
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