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