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