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.
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.
13 * @ingroup LockManager
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
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, ...))
28 /** @var Array Map of (locked key => lock type => count) */
29 protected $locksHeld = array();
30 /** @var Array Map Server connections (server name => resource) */
31 protected $conns = array();
33 protected $connTimeout; // float number of seconds
34 protected $session = ''; // random SHA-1 string
37 * Construct a new instance from configuration.
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
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]
48 * @param Array $config
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
56 if ( isset( $config['connTimeout'] ) ) {
57 $this->connTimeout
= $config['connTimeout'];
59 $this->connTimeout
= 3; // use some sane amount
63 for ( $i = 0; $i < 5; $i++
) {
64 $this->session
.= mt_rand( 0, 2147483647 );
66 $this->session
= wfBaseConvert( sha1( $this->session
), 16, 36, 31 );
69 protected function doLock( array $keys, $type ) {
70 $status = Status
::newGood();
72 $keysToLock = array();
73 // Get locks that need to be acquired (buckets => locks)...
74 foreach ( $keys as $key ) {
75 if ( isset( $this->locksHeld
[$key][$type] ) ) {
76 ++
$this->locksHeld
[$key][$type];
77 } elseif ( isset( $this->locksHeld
[$key][self
::LOCK_EX
] ) ) {
78 $this->locksHeld
[$key][$type] = 1;
80 $bucket = $this->getBucketFromKey( $key );
81 $keysToLock[$bucket][] = $key;
85 $lockedKeys = array(); // files locked in this attempt
86 // Attempt to acquire these locks...
87 foreach ( $keysToLock as $bucket => $keys ) {
88 // Try to acquire the locks for this bucket
89 $res = $this->doLockingRequestAll( $bucket, $keys, $type );
90 if ( $res === 'cantacquire' ) {
91 // Resources already locked by another process.
92 // Abort and unlock everything we just locked.
93 $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) );
94 $status->merge( $this->doUnlock( $lockedKeys, $type ) );
96 } elseif ( $res !== true ) {
97 // Couldn't contact any servers for this bucket.
98 // Abort and unlock everything we just locked.
99 $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) );
100 $status->merge( $this->doUnlock( $lockedKeys, $type ) );
103 // Record these locks as active
104 foreach ( $keys as $key ) {
105 $this->locksHeld
[$key][$type] = 1; // locked
107 // Keep track of what locks were made in this attempt
108 $lockedKeys = array_merge( $lockedKeys, $keys );
114 protected function doUnlock( array $keys, $type ) {
115 $status = Status
::newGood();
117 foreach ( $keys as $key ) {
118 if ( !isset( $this->locksHeld
[$key] ) ) {
119 $status->warning( 'lockmanager-notlocked', $key );
120 } elseif ( !isset( $this->locksHeld
[$key][$type] ) ) {
121 $status->warning( 'lockmanager-notlocked', $key );
123 --$this->locksHeld
[$key][$type];
124 if ( $this->locksHeld
[$key][$type] <= 0 ) {
125 unset( $this->locksHeld
[$key][$type] );
127 if ( !count( $this->locksHeld
[$key] ) ) {
128 unset( $this->locksHeld
[$key] ); // no SH or EX locks left for key
133 // Reference count the locks held and release locks when zero
134 if ( !count( $this->locksHeld
) ) {
135 $status->merge( $this->releaseLocks() );
142 * Get a connection to a lock server and acquire locks on $keys
144 * @param $lockSrv string
146 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
147 * @return bool Resources able to be locked
149 protected function doLockingRequest( $lockSrv, array $keys, $type ) {
150 if ( $type == self
::LOCK_SH
) { // reader locks
152 } elseif ( $type == self
::LOCK_EX
) { // writer locks
155 return true; // ok...
158 // Send out the command and get the response...
159 $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
161 return ( $response === 'ACQUIRED' );
165 * Send a command and get back the response
167 * @param $lockSrv string
168 * @param $action string
169 * @param $type string
170 * @param $values Array
171 * @return string|false
173 protected function sendCommand( $lockSrv, $action, $type, $values ) {
174 $conn = $this->getConnection( $lockSrv );
176 return false; // no connection
178 $authKey = $this->lockServers
[$lockSrv]['authKey'];
179 // Build of the command as a flat string...
180 $values = implode( '|', $values );
181 $key = sha1( $this->session
. $action . $type . $values . $authKey );
182 // Send out the command...
183 if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
186 // Get the response...
187 $response = fgets( $conn );
188 if ( $response === false ) {
191 return trim( $response );
195 * Attempt to acquire locks with the peers for a bucket
197 * @param $bucket integer
198 * @param $keys Array List of resource keys to lock
199 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
200 * @return bool|string One of (true, 'cantacquire', 'srverrors')
202 protected function doLockingRequestAll( $bucket, array $keys, $type ) {
203 $yesVotes = 0; // locks made on trustable servers
204 $votesLeft = count( $this->srvsByBucket
[$bucket] ); // remaining peers
205 $quorum = floor( $votesLeft/2 +
1 ); // simple majority
206 // Get votes for each peer, in order, until we have enough...
207 foreach ( $this->srvsByBucket
[$bucket] as $index => $lockSrv ) {
208 // Attempt to acquire the lock on this peer
209 if ( !$this->doLockingRequest( $lockSrv, $keys, $type ) ) {
210 return 'cantacquire'; // vetoed; resource locked
212 ++
$yesVotes; // success for this peer
213 if ( $yesVotes >= $quorum ) {
214 return true; // lock obtained
217 $votesNeeded = $quorum - $yesVotes;
218 if ( $votesNeeded > $votesLeft ) {
219 // In "trust cache" mode we don't have to meet the quorum
220 break; // short-circuit
223 // At this point, we must not have meet the quorum
224 return 'srverrors'; // not enough votes to ensure correctness
228 * Get (or reuse) a connection to a lock server
230 * @param $lockSrv string
233 protected function getConnection( $lockSrv ) {
234 if ( !isset( $this->conns
[$lockSrv] ) ) {
235 $cfg = $this->lockServers
[$lockSrv];
236 wfSuppressWarnings();
237 $errno = $errstr = '';
238 $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout
);
240 if ( $conn === false ) {
243 $sec = floor( $this->connTimeout
);
244 $usec = floor( ( $this->connTimeout
- floor( $this->connTimeout
) ) * 1e6
);
245 stream_set_timeout( $conn, $sec, $usec );
246 $this->conns
[$lockSrv] = $conn;
248 return $this->conns
[$lockSrv];
252 * Release all locks that this session is holding
256 protected function releaseLocks() {
257 $status = Status
::newGood();
258 foreach ( $this->conns
as $lockSrv => $conn ) {
259 $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() );
260 if ( $response !== 'RELEASED_ALL' ) {
261 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
268 * Get the bucket for lock key
270 * @param $key string (31 char hex key)
273 protected function getBucketFromKey( $key ) {
274 $prefix = substr( $key, 0, 2 ); // first 2 hex chars (8 bits)
275 return intval( base_convert( $prefix, 16, 10 ) ) %
count( $this->srvsByBucket
);
279 * Make sure remaining locks get cleared for sanity
281 function __destruct() {
282 $this->releaseLocks();
283 foreach ( $this->conns
as $lockSrv => $conn ) {