79102851c4988cb947ff1f4072828a7bf45cd8c5
[lhc/web/wiklou.git] / includes / filerepo / backend / lockmanager / LSLockManager.php
1 <?php
2 /**
3 * Version of LockManager based on using lock daemon servers.
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 * Manage locks using a lock daemon server.
26 *
27 * Version of LockManager based on using lock daemon servers.
28 * This is meant for multi-wiki systems that may share files.
29 * All locks are non-blocking, which avoids deadlocks.
30 *
31 * All lock requests for a resource, identified by a hash string, will map
32 * to one bucket. Each bucket maps to one or several peer servers, each
33 * running LockServerDaemon.php, listening on a designated TCP port.
34 * A majority of peers must agree for a lock to be acquired.
35 *
36 * @ingroup LockManager
37 * @since 1.19
38 */
39 class LSLockManager extends LockManager {
40 /** @var Array Mapping of lock types to the type actually used */
41 protected $lockTypeMap = array(
42 self::LOCK_SH => self::LOCK_SH,
43 self::LOCK_UW => self::LOCK_SH,
44 self::LOCK_EX => self::LOCK_EX
45 );
46
47 /** @var Array Map of server names to server config */
48 protected $lockServers; // (server name => server config array)
49 /** @var Array Map of bucket indexes to peer server lists */
50 protected $srvsByBucket; // (bucket index => (lsrv1, lsrv2, ...))
51
52 /** @var Array Map Server connections (server name => resource) */
53 protected $conns = array();
54
55 protected $connTimeout; // float number of seconds
56 protected $session = ''; // random SHA-1 string
57
58 /**
59 * Construct a new instance from configuration.
60 *
61 * $config paramaters include:
62 * 'lockServers' : Associative array of server names to configuration.
63 * Configuration is an associative array that includes:
64 * 'host' - IP address/hostname
65 * 'port' - TCP port
66 * 'authKey' - Secret string the lock server uses
67 * 'srvsByBucket' : Array of 1-16 consecutive integer keys, starting from 0,
68 * each having an odd-numbered list of server names (peers) as values.
69 * 'connTimeout' : Lock server connection attempt timeout. [optional]
70 *
71 * @param Array $config
72 */
73 public function __construct( array $config ) {
74 parent::__construct( $config );
75
76 $this->lockServers = $config['lockServers'];
77 // Sanitize srvsByBucket config to prevent PHP errors
78 $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
79 $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
80
81 if ( isset( $config['connTimeout'] ) ) {
82 $this->connTimeout = $config['connTimeout'];
83 } else {
84 $this->connTimeout = 3; // use some sane amount
85 }
86
87 $this->session = '';
88 for ( $i = 0; $i < 5; $i++ ) {
89 $this->session .= mt_rand( 0, 2147483647 );
90 }
91 $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 );
92 }
93
94 /**
95 * @see LockManager::doLock()
96 * @param $paths array
97 * @param $type int
98 * @return Status
99 */
100 protected function doLock( array $paths, $type ) {
101 $status = Status::newGood();
102
103 $pathsToLock = array();
104 // Get locks that need to be acquired (buckets => locks)...
105 foreach ( $paths as $path ) {
106 if ( isset( $this->locksHeld[$path][$type] ) ) {
107 ++$this->locksHeld[$path][$type];
108 } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
109 $this->locksHeld[$path][$type] = 1;
110 } else {
111 $bucket = $this->getBucketFromKey( $path );
112 $pathsToLock[$bucket][] = $path;
113 }
114 }
115
116 $lockedPaths = array(); // files locked in this attempt
117 // Attempt to acquire these locks...
118 foreach ( $pathsToLock as $bucket => $paths ) {
119 // Try to acquire the locks for this bucket
120 $res = $this->doLockingRequestAll( $bucket, $paths, $type );
121 if ( $res === 'cantacquire' ) {
122 // Resources already locked by another process.
123 // Abort and unlock everything we just locked.
124 foreach ( $paths as $path ) {
125 $status->fatal( 'lockmanager-fail-acquirelock', $path );
126 }
127 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
128 return $status;
129 } elseif ( $res !== true ) {
130 // Couldn't contact any servers for this bucket.
131 // Abort and unlock everything we just locked.
132 foreach ( $paths as $path ) {
133 $status->fatal( 'lockmanager-fail-acquirelock', $path );
134 }
135 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
136 return $status;
137 }
138 // Record these locks as active
139 foreach ( $paths as $path ) {
140 $this->locksHeld[$path][$type] = 1; // locked
141 }
142 // Keep track of what locks were made in this attempt
143 $lockedPaths = array_merge( $lockedPaths, $paths );
144 }
145
146 return $status;
147 }
148
149 /**
150 * @see LockManager::doUnlock()
151 * @param $paths array
152 * @param $type int
153 * @return Status
154 */
155 protected function doUnlock( array $paths, $type ) {
156 $status = Status::newGood();
157
158 foreach ( $paths as $path ) {
159 if ( !isset( $this->locksHeld[$path] ) ) {
160 $status->warning( 'lockmanager-notlocked', $path );
161 } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
162 $status->warning( 'lockmanager-notlocked', $path );
163 } else {
164 --$this->locksHeld[$path][$type];
165 if ( $this->locksHeld[$path][$type] <= 0 ) {
166 unset( $this->locksHeld[$path][$type] );
167 }
168 if ( !count( $this->locksHeld[$path] ) ) {
169 unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
170 }
171 }
172 }
173
174 // Reference count the locks held and release locks when zero
175 if ( !count( $this->locksHeld ) ) {
176 $status->merge( $this->releaseLocks() );
177 }
178
179 return $status;
180 }
181
182 /**
183 * Get a connection to a lock server and acquire locks on $paths
184 *
185 * @param $lockSrv string
186 * @param $paths Array
187 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
188 * @return bool Resources able to be locked
189 */
190 protected function doLockingRequest( $lockSrv, array $paths, $type ) {
191 if ( $type == self::LOCK_SH ) { // reader locks
192 $type = 'SH';
193 } elseif ( $type == self::LOCK_EX ) { // writer locks
194 $type = 'EX';
195 } else {
196 return true; // ok...
197 }
198
199 // Send out the command and get the response...
200 $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
201 $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
202
203 return ( $response === 'ACQUIRED' );
204 }
205
206 /**
207 * Send a command and get back the response
208 *
209 * @param $lockSrv string
210 * @param $action string
211 * @param $type string
212 * @param $values Array
213 * @return string|bool
214 */
215 protected function sendCommand( $lockSrv, $action, $type, $values ) {
216 $conn = $this->getConnection( $lockSrv );
217 if ( !$conn ) {
218 return false; // no connection
219 }
220 $authKey = $this->lockServers[$lockSrv]['authKey'];
221 // Build of the command as a flat string...
222 $values = implode( '|', $values );
223 $key = sha1( $this->session . $action . $type . $values . $authKey );
224 // Send out the command...
225 if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
226 return false;
227 }
228 // Get the response...
229 $response = fgets( $conn );
230 if ( $response === false ) {
231 return false;
232 }
233 return trim( $response );
234 }
235
236 /**
237 * Attempt to acquire locks with the peers for a bucket
238 *
239 * @param $bucket integer
240 * @param $paths Array List of resource keys to lock
241 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
242 * @return bool|string One of (true, 'cantacquire', 'srverrors')
243 */
244 protected function doLockingRequestAll( $bucket, array $paths, $type ) {
245 $yesVotes = 0; // locks made on trustable servers
246 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
247 $quorum = floor( $votesLeft/2 + 1 ); // simple majority
248 // Get votes for each peer, in order, until we have enough...
249 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
250 // Attempt to acquire the lock on this peer
251 if ( !$this->doLockingRequest( $lockSrv, $paths, $type ) ) {
252 return 'cantacquire'; // vetoed; resource locked
253 }
254 ++$yesVotes; // success for this peer
255 if ( $yesVotes >= $quorum ) {
256 return true; // lock obtained
257 }
258 --$votesLeft;
259 $votesNeeded = $quorum - $yesVotes;
260 if ( $votesNeeded > $votesLeft ) {
261 // In "trust cache" mode we don't have to meet the quorum
262 break; // short-circuit
263 }
264 }
265 // At this point, we must not have meet the quorum
266 return 'srverrors'; // not enough votes to ensure correctness
267 }
268
269 /**
270 * Get (or reuse) a connection to a lock server
271 *
272 * @param $lockSrv string
273 * @return resource
274 */
275 protected function getConnection( $lockSrv ) {
276 if ( !isset( $this->conns[$lockSrv] ) ) {
277 $cfg = $this->lockServers[$lockSrv];
278 wfSuppressWarnings();
279 $errno = $errstr = '';
280 $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout );
281 wfRestoreWarnings();
282 if ( $conn === false ) {
283 return null;
284 }
285 $sec = floor( $this->connTimeout );
286 $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 );
287 stream_set_timeout( $conn, $sec, $usec );
288 $this->conns[$lockSrv] = $conn;
289 }
290 return $this->conns[$lockSrv];
291 }
292
293 /**
294 * Release all locks that this session is holding
295 *
296 * @return Status
297 */
298 protected function releaseLocks() {
299 $status = Status::newGood();
300 foreach ( $this->conns as $lockSrv => $conn ) {
301 $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() );
302 if ( $response !== 'RELEASED_ALL' ) {
303 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
304 }
305 }
306 return $status;
307 }
308
309 /**
310 * Get the bucket for resource path.
311 * This should avoid throwing any exceptions.
312 *
313 * @param $path string
314 * @return integer
315 */
316 protected function getBucketFromKey( $path ) {
317 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
318 return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->srvsByBucket );
319 }
320
321 /**
322 * Make sure remaining locks get cleared for sanity
323 */
324 function __destruct() {
325 $this->releaseLocks();
326 foreach ( $this->conns as $conn ) {
327 fclose( $conn );
328 }
329 }
330 }