Merge "Add Special:Login and Special:Logout as aliases."
[lhc/web/wiklou.git] / includes / filebackend / lockmanager / QuorumLockManager.php
1 <?php
2 /**
3 * Version of LockManager that uses a quorum from peer servers for 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
24 /**
25 * Version of LockManager that uses a quorum from peer servers for locks.
26 * The resource space can also be sharded into separate peer groups.
27 *
28 * @ingroup LockManager
29 * @since 1.20
30 */
31 abstract class QuorumLockManager extends LockManager {
32 /** @var Array Map of bucket indexes to peer server lists */
33 protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...))
34
35 /**
36 * @see LockManager::doLock()
37 * @param $paths array
38 * @param $type int
39 * @return Status
40 */
41 final protected function doLock( array $paths, $type ) {
42 $status = Status::newGood();
43
44 $pathsToLock = array(); // (bucket => paths)
45 // Get locks that need to be acquired (buckets => locks)...
46 foreach ( $paths as $path ) {
47 if ( isset( $this->locksHeld[$path][$type] ) ) {
48 ++$this->locksHeld[$path][$type];
49 } else {
50 $bucket = $this->getBucketFromPath( $path );
51 $pathsToLock[$bucket][] = $path;
52 }
53 }
54
55 $lockedPaths = array(); // files locked in this attempt
56 // Attempt to acquire these locks...
57 foreach ( $pathsToLock as $bucket => $paths ) {
58 // Try to acquire the locks for this bucket
59 $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
60 if ( !$status->isOK() ) {
61 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
62 return $status;
63 }
64 // Record these locks as active
65 foreach ( $paths as $path ) {
66 $this->locksHeld[$path][$type] = 1; // locked
67 }
68 // Keep track of what locks were made in this attempt
69 $lockedPaths = array_merge( $lockedPaths, $paths );
70 }
71
72 return $status;
73 }
74
75 /**
76 * @see LockManager::doUnlock()
77 * @param $paths array
78 * @param $type int
79 * @return Status
80 */
81 final protected function doUnlock( array $paths, $type ) {
82 $status = Status::newGood();
83
84 $pathsToUnlock = array();
85 foreach ( $paths as $path ) {
86 if ( !isset( $this->locksHeld[$path][$type] ) ) {
87 $status->warning( 'lockmanager-notlocked', $path );
88 } else {
89 --$this->locksHeld[$path][$type];
90 // Reference count the locks held and release locks when zero
91 if ( $this->locksHeld[$path][$type] <= 0 ) {
92 unset( $this->locksHeld[$path][$type] );
93 $bucket = $this->getBucketFromPath( $path );
94 $pathsToUnlock[$bucket][] = $path;
95 }
96 if ( !count( $this->locksHeld[$path] ) ) {
97 unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
98 }
99 }
100 }
101
102 // Remove these specific locks if possible, or at least release
103 // all locks once this process is currently not holding any locks.
104 foreach ( $pathsToUnlock as $bucket => $paths ) {
105 $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
106 }
107 if ( !count( $this->locksHeld ) ) {
108 $status->merge( $this->releaseAllLocks() );
109 }
110
111 return $status;
112 }
113
114 /**
115 * Attempt to acquire locks with the peers for a bucket.
116 * This is all or nothing; if any key is locked then this totally fails.
117 *
118 * @param $bucket integer
119 * @param array $paths List of resource keys to lock
120 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
121 * @return Status
122 */
123 final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
124 $status = Status::newGood();
125
126 $yesVotes = 0; // locks made on trustable servers
127 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
128 $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
129 // Get votes for each peer, in order, until we have enough...
130 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
131 if ( !$this->isServerUp( $lockSrv ) ) {
132 --$votesLeft;
133 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
134 continue; // server down?
135 }
136 // Attempt to acquire the lock on this peer
137 $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
138 if ( !$status->isOK() ) {
139 return $status; // vetoed; resource locked
140 }
141 ++$yesVotes; // success for this peer
142 if ( $yesVotes >= $quorum ) {
143 return $status; // lock obtained
144 }
145 --$votesLeft;
146 $votesNeeded = $quorum - $yesVotes;
147 if ( $votesNeeded > $votesLeft ) {
148 break; // short-circuit
149 }
150 }
151 // At this point, we must not have met the quorum
152 $status->setResult( false );
153
154 return $status;
155 }
156
157 /**
158 * Attempt to release locks with the peers for a bucket
159 *
160 * @param $bucket integer
161 * @param array $paths List of resource keys to lock
162 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
163 * @return Status
164 */
165 final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
166 $status = Status::newGood();
167
168 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
169 if ( !$this->isServerUp( $lockSrv ) ) {
170 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
171 // Attempt to release the lock on this peer
172 } else {
173 $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
174 }
175 }
176
177 return $status;
178 }
179
180 /**
181 * Get the bucket for resource path.
182 * This should avoid throwing any exceptions.
183 *
184 * @param $path string
185 * @return integer
186 */
187 protected function getBucketFromPath( $path ) {
188 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
189 return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
190 }
191
192 /**
193 * Check if a lock server is up
194 *
195 * @param $lockSrv string
196 * @return bool
197 */
198 abstract protected function isServerUp( $lockSrv );
199
200 /**
201 * Get a connection to a lock server and acquire locks on $paths
202 *
203 * @param $lockSrv string
204 * @param $paths array
205 * @param $type integer
206 * @return Status
207 */
208 abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
209
210 /**
211 * Get a connection to a lock server and release locks on $paths.
212 *
213 * Subclasses must effectively implement this or releaseAllLocks().
214 *
215 * @param $lockSrv string
216 * @param $paths array
217 * @param $type integer
218 * @return Status
219 */
220 abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
221
222 /**
223 * Release all locks that this session is holding.
224 *
225 * Subclasses must effectively implement this or freeLocksOnServer().
226 *
227 * @return Status
228 */
229 abstract protected function releaseAllLocks();
230 }