DBLockManager.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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. use Wikimedia\Rdbms\Database;
  24. use Wikimedia\Rdbms\IDatabase;
  25. use Wikimedia\Rdbms\DBError;
  26. /**
  27. * Version of LockManager based on using named/row DB locks.
  28. *
  29. * This is meant for multi-wiki systems that may share files.
  30. *
  31. * All lock requests for a resource, identified by a hash string, will map to one bucket.
  32. * Each bucket maps to one or several peer DBs, each on their own server.
  33. * A majority of peer DBs must agree for a lock to be acquired.
  34. *
  35. * Caching is used to avoid hitting servers that are down.
  36. *
  37. * @ingroup LockManager
  38. * @since 1.19
  39. */
  40. abstract class DBLockManager extends QuorumLockManager {
  41. /** @var array[]|IDatabase[] Map of (DB names => server config or IDatabase) */
  42. protected $dbServers; // (DB name => server config array)
  43. /** @var BagOStuff */
  44. protected $statusCache;
  45. protected $lockExpiry; // integer number of seconds
  46. protected $safeDelay; // integer number of seconds
  47. /** @var IDatabase[] Map Database connections (DB name => Database) */
  48. protected $conns = [];
  49. /**
  50. * Construct a new instance from configuration.
  51. *
  52. * @param array $config Parameters include:
  53. * - dbServers : Associative array of DB names to server configuration.
  54. * Configuration is an associative array that includes:
  55. * - host : DB server name
  56. * - dbname : DB name
  57. * - type : DB type (mysql,postgres,...)
  58. * - user : DB user
  59. * - password : DB user password
  60. * - tablePrefix : DB table prefix
  61. * - flags : DB flags; bitfield of IDatabase::DBO_* constants
  62. * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
  63. * each having an odd-numbered list of DB names (peers) as values.
  64. * - lockExpiry : Lock timeout (seconds) for dropped connections. [optional]
  65. * This tells the DB server how long to wait before assuming
  66. * connection failure and releasing all the locks for a session.
  67. * - srvCache : A BagOStuff instance using APC or the like.
  68. */
  69. public function __construct( array $config ) {
  70. parent::__construct( $config );
  71. $this->dbServers = $config['dbServers'];
  72. // Sanitize srvsByBucket config to prevent PHP errors
  73. $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
  74. $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
  75. if ( isset( $config['lockExpiry'] ) ) {
  76. $this->lockExpiry = $config['lockExpiry'];
  77. } else {
  78. $met = ini_get( 'max_execution_time' );
  79. $this->lockExpiry = $met ?: 60; // use some sane amount if 0
  80. }
  81. $this->safeDelay = ( $this->lockExpiry <= 0 )
  82. ? 60 // pick a safe-ish number to match DB timeout default
  83. : $this->lockExpiry; // cover worst case
  84. // Tracks peers that couldn't be queried recently to avoid lengthy
  85. // connection timeouts. This is useless if each bucket has one peer.
  86. $this->statusCache = $config['srvCache'] ?? new HashBagOStuff();
  87. }
  88. /**
  89. * @todo change this code to work in one batch
  90. * @param string $lockSrv
  91. * @param array $pathsByType
  92. * @return StatusValue
  93. */
  94. protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
  95. $status = StatusValue::newGood();
  96. foreach ( $pathsByType as $type => $paths ) {
  97. $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
  98. }
  99. return $status;
  100. }
  101. abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
  102. protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
  103. return StatusValue::newGood();
  104. }
  105. /**
  106. * @see QuorumLockManager::isServerUp()
  107. * @param string $lockSrv
  108. * @return bool
  109. */
  110. protected function isServerUp( $lockSrv ) {
  111. if ( !$this->cacheCheckFailures( $lockSrv ) ) {
  112. return false; // recent failure to connect
  113. }
  114. try {
  115. $this->getConnection( $lockSrv );
  116. } catch ( DBError $e ) {
  117. $this->cacheRecordFailure( $lockSrv );
  118. return false; // failed to connect
  119. }
  120. return true;
  121. }
  122. /**
  123. * Get (or reuse) a connection to a lock DB
  124. *
  125. * @param string $lockDb
  126. * @return IDatabase
  127. * @throws DBError
  128. * @throws UnexpectedValueException
  129. */
  130. protected function getConnection( $lockDb ) {
  131. if ( !isset( $this->conns[$lockDb] ) ) {
  132. if ( $this->dbServers[$lockDb] instanceof IDatabase ) {
  133. // Direct injected connection hande for $lockDB
  134. $db = $this->dbServers[$lockDb];
  135. } elseif ( is_array( $this->dbServers[$lockDb] ) ) {
  136. // Parameters to construct a new database connection
  137. $config = $this->dbServers[$lockDb];
  138. $config['flags'] = ( $config['flags'] ?? 0 );
  139. $config['flags'] &= ~( IDatabase::DBO_TRX | IDatabase::DBO_DEFAULT );
  140. $db = Database::factory( $config['type'], $config );
  141. } else {
  142. throw new UnexpectedValueException( "No server called '$lockDb'." );
  143. }
  144. # If the connection drops, try to avoid letting the DB rollback
  145. # and release the locks before the file operations are finished.
  146. # This won't handle the case of DB server restarts however.
  147. $options = [];
  148. if ( $this->lockExpiry > 0 ) {
  149. $options['connTimeout'] = $this->lockExpiry;
  150. }
  151. $db->setSessionOptions( $options );
  152. $this->initConnection( $lockDb, $db );
  153. $this->conns[$lockDb] = $db;
  154. }
  155. return $this->conns[$lockDb];
  156. }
  157. /**
  158. * Do additional initialization for new lock DB connection
  159. *
  160. * @param string $lockDb
  161. * @param IDatabase $db
  162. * @throws DBError
  163. */
  164. protected function initConnection( $lockDb, IDatabase $db ) {
  165. }
  166. /**
  167. * Checks if the DB has not recently had connection/query errors.
  168. * This just avoids wasting time on doomed connection attempts.
  169. *
  170. * @param string $lockDb
  171. * @return bool
  172. */
  173. protected function cacheCheckFailures( $lockDb ) {
  174. return ( $this->safeDelay > 0 )
  175. ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
  176. : true;
  177. }
  178. /**
  179. * Log a lock request failure to the cache
  180. *
  181. * @param string $lockDb
  182. * @return bool Success
  183. */
  184. protected function cacheRecordFailure( $lockDb ) {
  185. return ( $this->safeDelay > 0 )
  186. ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
  187. : true;
  188. }
  189. /**
  190. * Get a cache key for recent query misses for a DB
  191. *
  192. * @param string $lockDb
  193. * @return string
  194. */
  195. protected function getMissKey( $lockDb ) {
  196. return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
  197. }
  198. /**
  199. * Make sure remaining locks get cleared for sanity
  200. */
  201. function __destruct() {
  202. $this->releaseAllLocks();
  203. foreach ( $this->conns as $db ) {
  204. $db->close();
  205. }
  206. }
  207. }