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