Merge "Make Title::getFirstRevision() ignore the rev_timestamp index"
[lhc/web/wiklou.git] / includes / filebackend / lockmanager / MySqlLockManager.php
1 <?php
2
3 use Wikimedia\Rdbms\IDatabase;
4
5 /**
6 * MySQL version of DBLockManager that supports shared locks.
7 *
8 * Do NOT use this on connection handles that are also being used for anything
9 * else as the transaction isolation will be wrong and all the other changes will
10 * get rolled back when the locks release!
11 *
12 * All lock servers must have the innodb table defined in maintenance/locking/filelocks.sql.
13 * All locks are non-blocking, which avoids deadlocks.
14 *
15 * @ingroup LockManager
16 */
17 class MySqlLockManager extends DBLockManager {
18 /** @var array Mapping of lock types to the type actually used */
19 protected $lockTypeMap = [
20 self::LOCK_SH => self::LOCK_SH,
21 self::LOCK_UW => self::LOCK_SH,
22 self::LOCK_EX => self::LOCK_EX
23 ];
24
25 public function __construct( array $config ) {
26 parent::__construct( $config );
27
28 $this->session = substr( $this->session, 0, 31 ); // fit to field
29 }
30
31 protected function initConnection( $lockDb, IDatabase $db ) {
32 # Let this transaction see lock rows from other transactions
33 $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
34 # Do everything in a transaction as it all gets rolled back eventually
35 $db->startAtomic( __CLASS__ );
36 }
37
38 /**
39 * Get a connection to a lock DB and acquire locks on $paths.
40 * This does not use GET_LOCK() per https://bugs.mysql.com/bug.php?id=1118.
41 *
42 * @see DBLockManager::getLocksOnServer()
43 * @param string $lockSrv
44 * @param array $paths
45 * @param string $type
46 * @return StatusValue
47 */
48 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
49 $status = StatusValue::newGood();
50
51 $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
52
53 $keys = []; // list of hash keys for the paths
54 $data = []; // list of rows to insert
55 $checkEXKeys = []; // list of hash keys that this has no EX lock on
56 # Build up values for INSERT clause
57 foreach ( $paths as $path ) {
58 $key = $this->sha1Base36Absolute( $path );
59 $keys[] = $key;
60 $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
61 if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
62 $checkEXKeys[] = $key; // this has no EX lock on $key itself
63 }
64 }
65
66 # Block new writers (both EX and SH locks leave entries here)...
67 $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
68 # Actually do the locking queries...
69 if ( $type == self::LOCK_SH ) { // reader locks
70 # Bail if there are any existing writers...
71 if ( count( $checkEXKeys ) ) {
72 $blocked = $db->selectField(
73 'filelocks_exclusive',
74 '1',
75 [ 'fle_key' => $checkEXKeys ],
76 __METHOD__
77 );
78 } else {
79 $blocked = false;
80 }
81 # Other prospective writers that haven't yet updated filelocks_exclusive
82 # will recheck filelocks_shared after doing so and bail due to this entry.
83 } else { // writer locks
84 $encSession = $db->addQuotes( $this->session );
85 # Bail if there are any existing writers...
86 # This may detect readers, but the safe check for them is below.
87 # Note: if two writers come at the same time, both bail :)
88 $blocked = $db->selectField(
89 'filelocks_shared',
90 '1',
91 [ 'fls_key' => $keys, "fls_session != $encSession" ],
92 __METHOD__
93 );
94 if ( !$blocked ) {
95 # Build up values for INSERT clause
96 $data = [];
97 foreach ( $keys as $key ) {
98 $data[] = [ 'fle_key' => $key ];
99 }
100 # Block new readers/writers...
101 $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
102 # Bail if there are any existing readers...
103 $blocked = $db->selectField(
104 'filelocks_shared',
105 '1',
106 [ 'fls_key' => $keys, "fls_session != $encSession" ],
107 __METHOD__
108 );
109 }
110 }
111
112 if ( $blocked ) {
113 foreach ( $paths as $path ) {
114 $status->fatal( 'lockmanager-fail-acquirelock', $path );
115 }
116 }
117
118 return $status;
119 }
120
121 /**
122 * @see QuorumLockManager::releaseAllLocks()
123 * @return StatusValue
124 */
125 protected function releaseAllLocks() {
126 $status = StatusValue::newGood();
127
128 foreach ( $this->conns as $lockDb => $db ) {
129 if ( $db->trxLevel() ) { // in transaction
130 try {
131 $db->rollback( __METHOD__ ); // finish transaction and kill any rows
132 } catch ( DBError $e ) {
133 $status->fatal( 'lockmanager-fail-db-release', $lockDb );
134 }
135 }
136 }
137
138 return $status;
139 }
140 }