4 * Manage locks using a lock daemon server.
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.
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.
15 * @ingroup LockManager
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
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, ...))
31 /** @var Array Map Server connections (server name => resource) */
32 protected $conns = array();
34 protected $connTimeout; // float number of seconds
35 protected $session = ''; // random SHA-1 string
38 * Construct a new instance from configuration.
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
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]
50 * @param Array $config
52 public function __construct( array $config ) {
53 parent
::__construct( $config );
55 $this->lockServers
= $config['lockServers'];
56 // Sanitize srvsByBucket config to prevent PHP errors
57 $this->srvsByBucket
= array_filter( $config['srvsByBucket'], 'is_array' );
58 $this->srvsByBucket
= array_values( $this->srvsByBucket
); // consecutive
60 if ( isset( $config['connTimeout'] ) ) {
61 $this->connTimeout
= $config['connTimeout'];
63 $this->connTimeout
= 3; // use some sane amount
67 for ( $i = 0; $i < 5; $i++
) {
68 $this->session
.= mt_rand( 0, 2147483647 );
70 $this->session
= wfBaseConvert( sha1( $this->session
), 16, 36, 31 );
74 * @see LockManager::doLock()
77 protected function doLock( array $paths, $type ) {
78 $status = Status
::newGood();
80 $pathsToLock = array();
81 // Get locks that need to be acquired (buckets => locks)...
82 foreach ( $paths as $path ) {
83 if ( isset( $this->locksHeld
[$path][$type] ) ) {
84 ++
$this->locksHeld
[$path][$type];
85 } elseif ( isset( $this->locksHeld
[$path][self
::LOCK_EX
] ) ) {
86 $this->locksHeld
[$path][$type] = 1;
88 $bucket = $this->getBucketFromKey( $path );
89 $pathsToLock[$bucket][] = $path;
93 $lockedPaths = array(); // files locked in this attempt
94 // Attempt to acquire these locks...
95 foreach ( $pathsToLock as $bucket => $paths ) {
96 // Try to acquire the locks for this bucket
97 $res = $this->doLockingRequestAll( $bucket, $paths, $type );
98 if ( $res === 'cantacquire' ) {
99 // Resources already locked by another process.
100 // Abort and unlock everything we just locked.
101 foreach ( $paths as $path ) {
102 $status->fatal( 'lockmanager-fail-acquirelock', $path );
104 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
106 } elseif ( $res !== true ) {
107 // Couldn't contact any servers for this bucket.
108 // Abort and unlock everything we just locked.
109 foreach ( $paths as $path ) {
110 $status->fatal( 'lockmanager-fail-acquirelock', $path );
112 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
115 // Record these locks as active
116 foreach ( $paths as $path ) {
117 $this->locksHeld
[$path][$type] = 1; // locked
119 // Keep track of what locks were made in this attempt
120 $lockedPaths = array_merge( $lockedPaths, $paths );
127 * @see LockManager::doUnlock()
130 protected function doUnlock( array $paths, $type ) {
131 $status = Status
::newGood();
133 foreach ( $paths as $path ) {
134 if ( !isset( $this->locksHeld
[$path] ) ) {
135 $status->warning( 'lockmanager-notlocked', $path );
136 } elseif ( !isset( $this->locksHeld
[$path][$type] ) ) {
137 $status->warning( 'lockmanager-notlocked', $path );
139 --$this->locksHeld
[$path][$type];
140 if ( $this->locksHeld
[$path][$type] <= 0 ) {
141 unset( $this->locksHeld
[$path][$type] );
143 if ( !count( $this->locksHeld
[$path] ) ) {
144 unset( $this->locksHeld
[$path] ); // no SH or EX locks left for key
149 // Reference count the locks held and release locks when zero
150 if ( !count( $this->locksHeld
) ) {
151 $status->merge( $this->releaseLocks() );
158 * Get a connection to a lock server and acquire locks on $paths
160 * @param $lockSrv string
161 * @param $paths Array
162 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
163 * @return bool Resources able to be locked
165 protected function doLockingRequest( $lockSrv, array $paths, $type ) {
166 if ( $type == self
::LOCK_SH
) { // reader locks
168 } elseif ( $type == self
::LOCK_EX
) { // writer locks
171 return true; // ok...
174 // Send out the command and get the response...
175 $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
176 $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
178 return ( $response === 'ACQUIRED' );
182 * Send a command and get back the response
184 * @param $lockSrv string
185 * @param $action string
186 * @param $type string
187 * @param $values Array
188 * @return string|bool
190 protected function sendCommand( $lockSrv, $action, $type, $values ) {
191 $conn = $this->getConnection( $lockSrv );
193 return false; // no connection
195 $authKey = $this->lockServers
[$lockSrv]['authKey'];
196 // Build of the command as a flat string...
197 $values = implode( '|', $values );
198 $key = sha1( $this->session
. $action . $type . $values . $authKey );
199 // Send out the command...
200 if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
203 // Get the response...
204 $response = fgets( $conn );
205 if ( $response === false ) {
208 return trim( $response );
212 * Attempt to acquire locks with the peers for a bucket
214 * @param $bucket integer
215 * @param $paths Array List of resource keys to lock
216 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
217 * @return bool|string One of (true, 'cantacquire', 'srverrors')
219 protected function doLockingRequestAll( $bucket, array $paths, $type ) {
220 $yesVotes = 0; // locks made on trustable servers
221 $votesLeft = count( $this->srvsByBucket
[$bucket] ); // remaining peers
222 $quorum = floor( $votesLeft/2 +
1 ); // simple majority
223 // Get votes for each peer, in order, until we have enough...
224 foreach ( $this->srvsByBucket
[$bucket] as $lockSrv ) {
225 // Attempt to acquire the lock on this peer
226 if ( !$this->doLockingRequest( $lockSrv, $paths, $type ) ) {
227 return 'cantacquire'; // vetoed; resource locked
229 ++
$yesVotes; // success for this peer
230 if ( $yesVotes >= $quorum ) {
231 return true; // lock obtained
234 $votesNeeded = $quorum - $yesVotes;
235 if ( $votesNeeded > $votesLeft ) {
236 // In "trust cache" mode we don't have to meet the quorum
237 break; // short-circuit
240 // At this point, we must not have meet the quorum
241 return 'srverrors'; // not enough votes to ensure correctness
245 * Get (or reuse) a connection to a lock server
247 * @param $lockSrv string
250 protected function getConnection( $lockSrv ) {
251 if ( !isset( $this->conns
[$lockSrv] ) ) {
252 $cfg = $this->lockServers
[$lockSrv];
253 wfSuppressWarnings();
254 $errno = $errstr = '';
255 $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout
);
257 if ( $conn === false ) {
260 $sec = floor( $this->connTimeout
);
261 $usec = floor( ( $this->connTimeout
- floor( $this->connTimeout
) ) * 1e6
);
262 stream_set_timeout( $conn, $sec, $usec );
263 $this->conns
[$lockSrv] = $conn;
265 return $this->conns
[$lockSrv];
269 * Release all locks that this session is holding
273 protected function releaseLocks() {
274 $status = Status
::newGood();
275 foreach ( $this->conns
as $lockSrv => $conn ) {
276 $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() );
277 if ( $response !== 'RELEASED_ALL' ) {
278 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
285 * Get the bucket for resource path.
286 * This should avoid throwing any exceptions.
288 * @param $path string
291 protected function getBucketFromKey( $path ) {
292 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
293 return intval( base_convert( $prefix, 16, 10 ) ) %
count( $this->srvsByBucket
);
297 * Make sure remaining locks get cleared for sanity
299 function __destruct() {
300 $this->releaseLocks();
301 foreach ( $this->conns
as $conn ) {