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