Index: branches/FileBackend/phase3/maintenance/language/messages.inc |
— | — | @@ -1374,7 +1374,8 @@ |
1375 | 1375 | 'lockmanager-fail-releaselock', |
1376 | 1376 | 'lockmanager-fail-acquirelocks', |
1377 | 1377 | 'lockmanager-fail-db-bucket', |
1378 | | - 'lockmanager-fail-db-release' |
| 1378 | + 'lockmanager-fail-db-release', |
| 1379 | + 'lockmanager-fail-svr-release' |
1379 | 1380 | ), |
1380 | 1381 | |
1381 | 1382 | 'zip' => array( |
Index: branches/FileBackend/phase3/maintenance/locking/LockServerDaemon.php |
— | — | @@ -0,0 +1,414 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +if ( php_sapi_name() !== 'cli' ) { |
| 5 | + die( "This is not a valid entry point.\n" ); |
| 6 | +} |
| 7 | +error_reporting( E_ALL ); |
| 8 | + |
| 9 | +// Run the server... |
| 10 | +set_time_limit( 0 ); |
| 11 | +LockServerDaemon::init( |
| 12 | + getopt( '', array( |
| 13 | + 'address:', 'port:', 'authKey:', |
| 14 | + 'connTimeout::', 'lockTimeout::', 'maxClients::', 'maxBacklog::', 'maxLocks::', |
| 15 | + ) ) |
| 16 | +)->main(); |
| 17 | + |
| 18 | +/** |
| 19 | + * Simple lock server daemon that accepts lock/unlock requests. |
| 20 | + * This should not require MediaWiki setup or PHP files. |
| 21 | + */ |
| 22 | +class LockServerDaemon { |
| 23 | + /** @var resource */ |
| 24 | + protected $sock; // socket to listen/accept on |
| 25 | + /** @var Array */ |
| 26 | + protected $shLocks = array(); // (key => session => 1) |
| 27 | + /** @var Array */ |
| 28 | + protected $exLocks = array(); // (key => session) |
| 29 | + /** @var Array */ |
| 30 | + protected $sessions = array(); // (session => resource) |
| 31 | + /** @var Array */ |
| 32 | + protected $deadSessions = array(); // (session => UNIX timestamp) |
| 33 | + |
| 34 | + /** @var Array */ |
| 35 | + protected $sessionIndexSh = array(); // (session => key => 1) |
| 36 | + /** @var Array */ |
| 37 | + protected $sessionIndexEx = array(); // (session => key => 1) |
| 38 | + |
| 39 | + protected $authKey; // string key |
| 40 | + protected $connTimeout; // array ( 'sec' => integer, 'usec' => integer ) |
| 41 | + protected $lockTimeout; // integer number of seconds |
| 42 | + protected $maxLocks; // integer |
| 43 | + protected $maxClients; // integer |
| 44 | + |
| 45 | + protected $startTime; // integer UNIX timestamp |
| 46 | + protected $lockCount = 0; // integer |
| 47 | + protected $ticks = 0; // integer counter |
| 48 | + |
| 49 | + protected static $instance = null; |
| 50 | + |
| 51 | + /** |
| 52 | + * @params $config Array |
| 53 | + * @return LockServerDaemon |
| 54 | + */ |
| 55 | + public function init( array $config ) { |
| 56 | + if ( self::$instance ) { |
| 57 | + throw new Exception( 'LockServer already initialized.' ); |
| 58 | + } |
| 59 | + self::$instance = new self( $config ); |
| 60 | + return self::$instance; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * @params $config Array |
| 65 | + */ |
| 66 | + protected function __construct( array $config ) { |
| 67 | + $required = array( 'address', 'port', 'authKey' ); |
| 68 | + foreach ( $required as $par ) { |
| 69 | + if ( !isset( $config[$par] ) ) { |
| 70 | + throw new Exception( "Parameter '$par' must be specified." ); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + $this->authKey = $config['authKey']; |
| 75 | + $connTimeout = isset( $config['connTimeout'] ) |
| 76 | + ? $config['connTimeout'] |
| 77 | + : 1.5; |
| 78 | + $this->connTimeout = array( |
| 79 | + 'sec' => floor( $connTimeout ), |
| 80 | + 'usec' => floor( ( $connTimeout - floor( $connTimeout ) ) * 1e6 ) |
| 81 | + ); |
| 82 | + $this->lockTimeout = isset( $config['lockTimeout'] ) |
| 83 | + ? $config['lockTimeout'] |
| 84 | + : 60; |
| 85 | + $this->maxLocks = isset( $config['maxLocks'] ) |
| 86 | + ? $config['maxLocks'] |
| 87 | + : 5000; |
| 88 | + $this->maxClients = isset( $config['maxClients'] ) |
| 89 | + ? $config['maxClients'] |
| 90 | + : 100; |
| 91 | + $backlog = isset( $config['maxBacklog'] ) |
| 92 | + ? $config['maxBacklog'] |
| 93 | + : 10; |
| 94 | + |
| 95 | + if ( !function_exists( 'socket_create' ) ) { |
| 96 | + throw new Exception( "PHP sockets extension missing from PHP CLI mode." ); |
| 97 | + } |
| 98 | + $sock = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); |
| 99 | + if ( $sock === false ) { |
| 100 | + throw new Exception( "socket_create(): " . socket_strerror( socket_last_error() ) ); |
| 101 | + } |
| 102 | + socket_set_option( $sock, SOL_SOCKET, SO_REUSEADDR, 1 ); // bypass 2MLS |
| 103 | + if ( socket_bind( $sock, $config['address'], $config['port'] ) === false ) { |
| 104 | + throw new Exception( "socket_bind(): " . |
| 105 | + socket_strerror( socket_last_error( $sock ) ) ); |
| 106 | + } elseif ( socket_listen( $sock, $backlog ) === false ) { |
| 107 | + throw new Exception( "socket_listen(): " . |
| 108 | + socket_strerror( socket_last_error( $sock ) ) ); |
| 109 | + } |
| 110 | + $this->sock = $sock; |
| 111 | + |
| 112 | + $this->startTime = time(); |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * @return void |
| 117 | + */ |
| 118 | + public function main() { |
| 119 | + // Create a list of all the clients that will be connected to us. |
| 120 | + $clients = array( $this->sock ); // start off with listening socket |
| 121 | + do { |
| 122 | + // Create a copy, so $clients doesn't get modified by socket_select() |
| 123 | + $read = $clients; // clients-with-data |
| 124 | + // Get a list of all the clients that have data to be read from |
| 125 | + $changed = socket_select( $read, $write = NULL, $except = NULL, NULL ); |
| 126 | + if ( $changed === false ) { |
| 127 | + trigger_error( 'socket_listen(): ' . socket_strerror( socket_last_error() ) ); |
| 128 | + continue; |
| 129 | + } elseif ( $changed < 1 ) { |
| 130 | + continue; // wait |
| 131 | + } |
| 132 | + // Check if there is a client trying to connect... |
| 133 | + if ( in_array( $this->sock, $read ) && count( $clients ) < $this->maxClients ) { |
| 134 | + // Accept the new client... |
| 135 | + $newsock = socket_accept( $this->sock ); |
| 136 | + socket_set_option( $newsock, SOL_SOCKET, SO_RCVTIMEO, $this->connTimeout ); |
| 137 | + socket_set_option( $newsock, SOL_SOCKET, SO_SNDTIMEO, $this->connTimeout ); |
| 138 | + $clients[] = $newsock; |
| 139 | + // Remove the listening socket from the clients-with-data array... |
| 140 | + $key = array_search( $this->sock, $read ); |
| 141 | + unset( $read[$key] ); |
| 142 | + } |
| 143 | + // Loop through all the clients that have data to read... |
| 144 | + foreach ( $read as $read_sock ) { |
| 145 | + // Read until newline or 65535 bytes are recieved. |
| 146 | + // socket_read show errors when the client is disconnected. |
| 147 | + $data = @socket_read( $read_sock, 65535, PHP_NORMAL_READ ); |
| 148 | + // Check if the client is disconnected |
| 149 | + if ( $data === false ) { |
| 150 | + // Remove client for $clients array |
| 151 | + $key = array_search( $read_sock, $clients ); |
| 152 | + unset( $clients[$key] ); |
| 153 | + // Remove socket's session from tracking (if it exists) |
| 154 | + $session = array_search( $read_sock, $this->sessions ); |
| 155 | + if ( $session !== false ) { |
| 156 | + unset( $this->sessions[$session] ); |
| 157 | + // Record recently killed sessions that still have locks |
| 158 | + if ( isset( $this->sessionIndexSh[$session] ) |
| 159 | + || isset( $this->sessionIndexEx[$session] ) ) |
| 160 | + { |
| 161 | + $this->deadSessions[$session] = time(); |
| 162 | + } |
| 163 | + } |
| 164 | + } else { |
| 165 | + // Perform the requested command... |
| 166 | + $response = $this->doCommand( trim( $data ), $read_sock ); |
| 167 | + // Send the response to the client... |
| 168 | + if ( socket_write( $read_sock, "$response\n" ) === false ) { |
| 169 | + trigger_error( 'socket_write(): ' . |
| 170 | + socket_strerror( socket_last_error( $read_sock ) ) ); |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + // Prune dead locks every 10 socket events... |
| 175 | + if ( ++$this->ticks >= 9 ) { |
| 176 | + $this->ticks = 0; |
| 177 | + $this->purgeExpiredLocks(); |
| 178 | + } |
| 179 | + } while ( true ); |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * @param $data string |
| 184 | + * @param $sourceSock resource |
| 185 | + * @return string |
| 186 | + */ |
| 187 | + protected function doCommand( $data, $sourceSock ) { |
| 188 | + $cmdArr = $this->getCommand( $data ); |
| 189 | + if ( is_string( $cmdArr ) ) { |
| 190 | + return $cmdArr; // error |
| 191 | + } |
| 192 | + list( $function, $session, $type, $resources ) = $cmdArr; |
| 193 | + // On first command, track the session => sock correspondence |
| 194 | + if ( !isset( $this->sessions[$session] ) ) { |
| 195 | + $this->sessions[$session] = $sourceSock; |
| 196 | + } |
| 197 | + if ( $function === 'ACQUIRE' ) { |
| 198 | + return $this->lock( $session, $type, $resources ); |
| 199 | + } elseif ( $function === 'RELEASE' ) { |
| 200 | + return $this->unlock( $session, $type, $resources ); |
| 201 | + } elseif ( $function === 'RELEASE_ALL' ) { |
| 202 | + return $this->release( $session ); |
| 203 | + } elseif ( $function === 'STAT' ) { |
| 204 | + return $this->stat(); |
| 205 | + } |
| 206 | + return 'INTERNAL_ERROR'; |
| 207 | + } |
| 208 | + |
| 209 | + /** |
| 210 | + * @param $data string |
| 211 | + * @return Array |
| 212 | + */ |
| 213 | + protected function getCommand( $data ) { |
| 214 | + $m = explode( ':', $data ); // <session, key, command, type, values> |
| 215 | + if ( count( $m ) == 5 ) { |
| 216 | + list( $session, $key, $command, $type, $values ) = $m; |
| 217 | + if ( sha1( $session . $command . $type . $values . $this->authKey ) !== $key ) { |
| 218 | + return 'BAD_KEY'; |
| 219 | + } elseif ( strlen( $session ) !== 40 ) { |
| 220 | + return 'BAD_SESSION'; |
| 221 | + } |
| 222 | + $values = explode( '|', $values ); |
| 223 | + if ( $command === 'ACQUIRE' ) { |
| 224 | + $needsLockArgs = true; |
| 225 | + } elseif ( $command === 'RELEASE' ) { |
| 226 | + $needsLockArgs = true; |
| 227 | + } elseif ( $command === 'RELEASE_ALL' ) { |
| 228 | + $needsLockArgs = false; |
| 229 | + } elseif ( $command === 'STAT' ) { |
| 230 | + $needsLockArgs = false; |
| 231 | + } else { |
| 232 | + return 'BAD_COMMAND'; |
| 233 | + } |
| 234 | + if ( $needsLockArgs ) { |
| 235 | + if ( $type !== 'SH' && $type !== 'EX' ) { |
| 236 | + return 'BAD_TYPE'; |
| 237 | + } |
| 238 | + foreach ( $values as $value ) { |
| 239 | + if ( strlen( $value ) !== 40 ) { |
| 240 | + return 'BAD_FORMAT'; |
| 241 | + } |
| 242 | + } |
| 243 | + } |
| 244 | + return array( $command, $session, $type, $values ); |
| 245 | + } |
| 246 | + return 'BAD_FORMAT'; |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * @param $session string |
| 251 | + * @param $type string |
| 252 | + * @param $keys Array |
| 253 | + * @return string |
| 254 | + */ |
| 255 | + protected function lock( $session, $type, $keys ) { |
| 256 | + if ( $this->lockCount >= $this->maxLocks ) { |
| 257 | + return 'TOO_MANY_LOCKS'; |
| 258 | + } |
| 259 | + if ( $type === 'SH' ) { |
| 260 | + // Check if any keys are already write-locked... |
| 261 | + foreach ( $keys as $key ) { |
| 262 | + if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] !== $session ) { |
| 263 | + return 'CANT_ACQUIRE'; |
| 264 | + } |
| 265 | + } |
| 266 | + // Acquire the read-locks... |
| 267 | + foreach ( $keys as $key ) { |
| 268 | + $this->set_sh_lock( $key, $session ); |
| 269 | + } |
| 270 | + return 'ACQUIRED'; |
| 271 | + } elseif ( $type === 'EX' ) { |
| 272 | + // Check if any keys are already read-locked or write-locked... |
| 273 | + foreach ( $keys as $key ) { |
| 274 | + if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] !== $session ) { |
| 275 | + return 'CANT_ACQUIRE'; |
| 276 | + } |
| 277 | + if ( isset( $this->shLocks[$key] ) ) { |
| 278 | + foreach ( $this->shLocks[$key] as $otherSession => $x ) { |
| 279 | + if ( $otherSession !== $session ) { |
| 280 | + return 'CANT_ACQUIRE'; |
| 281 | + } |
| 282 | + } |
| 283 | + } |
| 284 | + } |
| 285 | + // Acquire the write-locks... |
| 286 | + foreach ( $keys as $key ) { |
| 287 | + $this->set_ex_lock( $key, $session ); |
| 288 | + } |
| 289 | + return 'ACQUIRED'; |
| 290 | + } |
| 291 | + return 'INTERNAL_ERROR'; |
| 292 | + } |
| 293 | + |
| 294 | + /** |
| 295 | + * @param $session string |
| 296 | + * @param $type string |
| 297 | + * @param $keys Array |
| 298 | + * @return string |
| 299 | + */ |
| 300 | + protected function unlock( $session, $type, $keys ) { |
| 301 | + if ( $type === 'SH' ) { |
| 302 | + foreach ( $keys as $key ) { |
| 303 | + $this->unset_sh_lock( $key, $session ); |
| 304 | + } |
| 305 | + return 'RELEASED'; |
| 306 | + } elseif ( $type === 'EX' ) { |
| 307 | + foreach ( $keys as $key ) { |
| 308 | + $this->unset_ex_lock( $key, $session ); |
| 309 | + } |
| 310 | + return 'RELEASED'; |
| 311 | + } |
| 312 | + return 'INTERNAL_ERROR'; |
| 313 | + } |
| 314 | + |
| 315 | + /** |
| 316 | + * @param $session string |
| 317 | + * @return string |
| 318 | + */ |
| 319 | + protected function release( $session ) { |
| 320 | + if ( isset( $this->sessionIndexSh[$session] ) ) { |
| 321 | + foreach ( $this->sessionIndexSh[$session] as $key => $x ) { |
| 322 | + $this->unset_sh_lock( $key, $session ); |
| 323 | + } |
| 324 | + } |
| 325 | + if ( isset( $this->sessionIndexEx[$session] ) ) { |
| 326 | + foreach ( $this->sessionIndexEx[$session] as $key => $x ) { |
| 327 | + $this->unset_ex_lock( $key, $session ); |
| 328 | + } |
| 329 | + } |
| 330 | + return 'RELEASED_ALL'; |
| 331 | + } |
| 332 | + |
| 333 | + /** |
| 334 | + * @return string |
| 335 | + */ |
| 336 | + protected function stat() { |
| 337 | + return ( time() - $this->startTime ) . ':' . memory_get_usage(); |
| 338 | + } |
| 339 | + |
| 340 | + /** |
| 341 | + * Clear locks for sessions that have been dead for a while |
| 342 | + * |
| 343 | + * @return void |
| 344 | + */ |
| 345 | + protected function purgeExpiredLocks() { |
| 346 | + $now = time(); |
| 347 | + foreach ( $this->deadSessions as $session => $timestamp ) { |
| 348 | + if ( ( $now - $timestamp ) > $this->lockTimeout ) { |
| 349 | + $this->release( $session ); |
| 350 | + unset( $this->deadSessions[$session] ); |
| 351 | + } |
| 352 | + } |
| 353 | + } |
| 354 | + |
| 355 | + /** |
| 356 | + * @param $key string |
| 357 | + * @param $session string |
| 358 | + * @return void |
| 359 | + */ |
| 360 | + protected function set_sh_lock( $key, $session ) { |
| 361 | + if ( !isset( $this->shLocks[$key][$session] ) ) { |
| 362 | + $this->shLocks[$key][$session] = 1; |
| 363 | + $this->sessionIndexSh[$session][$key] = 1; |
| 364 | + ++$this->lockCount; // we are adding a lock |
| 365 | + } |
| 366 | + } |
| 367 | + |
| 368 | + /** |
| 369 | + * @param $key string |
| 370 | + * @param $session string |
| 371 | + * @return void |
| 372 | + */ |
| 373 | + protected function set_ex_lock( $key, $session ) { |
| 374 | + if ( !isset( $this->exLocks[$key][$session] ) ) { |
| 375 | + $this->exLocks[$key] = $session; |
| 376 | + $this->sessionIndexEx[$session][$key] = 1; |
| 377 | + ++$this->lockCount; // we are adding a lock |
| 378 | + } |
| 379 | + } |
| 380 | + |
| 381 | + /** |
| 382 | + * @param $key string |
| 383 | + * @param $session string |
| 384 | + * @return void |
| 385 | + */ |
| 386 | + protected function unset_sh_lock( $key, $session ) { |
| 387 | + if ( isset( $this->shLocks[$key][$session] ) ) { |
| 388 | + unset( $this->shLocks[$key][$session] ); |
| 389 | + if ( !count( $this->shLocks[$key] ) ) { |
| 390 | + unset( $this->shLocks[$key] ); |
| 391 | + } |
| 392 | + unset( $this->sessionIndexSh[$session][$key] ); |
| 393 | + if ( !count( $this->sessionIndexSh[$session] ) ) { |
| 394 | + unset( $this->sessionIndexSh[$session] ); |
| 395 | + } |
| 396 | + --$this->lockCount; |
| 397 | + } |
| 398 | + } |
| 399 | + |
| 400 | + /** |
| 401 | + * @param $key string |
| 402 | + * @param $session string |
| 403 | + * @return void |
| 404 | + */ |
| 405 | + protected function unset_ex_lock( $key, $session ) { |
| 406 | + if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] === $session ) { |
| 407 | + unset( $this->exLocks[$key] ); |
| 408 | + unset( $this->sessionIndexEx[$session][$key] ); |
| 409 | + if ( !count( $this->sessionIndexEx[$session] ) ) { |
| 410 | + unset( $this->sessionIndexEx[$session] ); |
| 411 | + } |
| 412 | + --$this->lockCount; |
| 413 | + } |
| 414 | + } |
| 415 | +} |
Property changes on: branches/FileBackend/phase3/maintenance/locking/LockServerDaemon.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 416 | + native |
Index: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LSLockManager.php |
— | — | @@ -0,0 +1,285 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Version of LockManager based on using lock daemon servers. |
| 6 | + * This is meant for multi-wiki systems that may share files. |
| 7 | + * All locks are non-blocking, which avoids deadlocks. |
| 8 | + * |
| 9 | + * All lock requests for a resource, identified by a hash string, will map |
| 10 | + * to one bucket. Each bucket maps to one or several peer servers, each |
| 11 | + * running LockServerDaemon.php, listening on a designated TCP port. |
| 12 | + * A majority of peers must agree for a lock to be acquired. |
| 13 | + */ |
| 14 | +class LSLockManager extends LockManager { |
| 15 | + /** @var Array Mapping of lock types to the type actually used */ |
| 16 | + protected $lockTypeMap = array( |
| 17 | + self::LOCK_SH => self::LOCK_SH, |
| 18 | + self::LOCK_UW => self::LOCK_SH, |
| 19 | + self::LOCK_EX => self::LOCK_EX |
| 20 | + ); |
| 21 | + |
| 22 | + /** @var Array Map of server names to server config */ |
| 23 | + protected $lockServers; // (server name => server config array) |
| 24 | + /** @var Array Map of bucket indexes to peer server lists */ |
| 25 | + protected $srvsByBucket; // (bucket index => (lsrv1, lsrv2, ...)) |
| 26 | + |
| 27 | + /** @var Array Map of (locked key => lock type => count) */ |
| 28 | + protected $locksHeld = array(); |
| 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 | + * $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 = sha1( $this->session ); |
| 66 | + } |
| 67 | + |
| 68 | + protected function doLock( array $keys, $type ) { |
| 69 | + $status = Status::newGood(); |
| 70 | + |
| 71 | + $keysToLock = array(); |
| 72 | + // Get locks that need to be acquired (buckets => locks)... |
| 73 | + foreach ( $keys as $key ) { |
| 74 | + if ( isset( $this->locksHeld[$key][$type] ) ) { |
| 75 | + ++$this->locksHeld[$key][$type]; |
| 76 | + } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { |
| 77 | + $this->locksHeld[$key][$type] = 1; |
| 78 | + } else { |
| 79 | + $bucket = $this->getBucketFromKey( $key ); |
| 80 | + $keysToLock[$bucket][] = $key; |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + $lockedKeys = array(); // files locked in this attempt |
| 85 | + // Attempt to acquire these locks... |
| 86 | + foreach ( $keysToLock as $bucket => $keys ) { |
| 87 | + // Try to acquire the locks for this bucket |
| 88 | + $res = $this->doLockingRequestAll( $bucket, $keys, $type ); |
| 89 | + if ( $res === 'cantacquire' ) { |
| 90 | + // Resources already locked by another process. |
| 91 | + // Abort and unlock everything we just locked. |
| 92 | + $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) ); |
| 93 | + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
| 94 | + return $status; |
| 95 | + } elseif ( $res !== true ) { |
| 96 | + // Couldn't contact any servers for this bucket. |
| 97 | + // Abort and unlock everything we just locked. |
| 98 | + $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) ); |
| 99 | + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
| 100 | + return $status; |
| 101 | + } |
| 102 | + // Record these locks as active |
| 103 | + foreach ( $keys as $key ) { |
| 104 | + $this->locksHeld[$key][$type] = 1; // locked |
| 105 | + } |
| 106 | + // Keep track of what locks were made in this attempt |
| 107 | + $lockedKeys = array_merge( $lockedKeys, $keys ); |
| 108 | + } |
| 109 | + |
| 110 | + return $status; |
| 111 | + } |
| 112 | + |
| 113 | + protected function doUnlock( array $keys, $type ) { |
| 114 | + $status = Status::newGood(); |
| 115 | + |
| 116 | + foreach ( $keys as $key ) { |
| 117 | + if ( !isset( $this->locksHeld[$key] ) ) { |
| 118 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 119 | + } elseif ( !isset( $this->locksHeld[$key][$type] ) ) { |
| 120 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 121 | + } else { |
| 122 | + --$this->locksHeld[$key][$type]; |
| 123 | + if ( $this->locksHeld[$key][$type] <= 0 ) { |
| 124 | + unset( $this->locksHeld[$key][$type] ); |
| 125 | + } |
| 126 | + if ( !count( $this->locksHeld[$key] ) ) { |
| 127 | + unset( $this->locksHeld[$key] ); // no SH or EX locks left for key |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + // Reference count the locks held and release locks when zero |
| 133 | + if ( !count( $this->locksHeld ) ) { |
| 134 | + $status->merge( $this->releaseLocks() ); |
| 135 | + } |
| 136 | + |
| 137 | + return $status; |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Get a connection to a lock server and acquire locks on $keys. |
| 142 | + * |
| 143 | + * @param $lockSrv string |
| 144 | + * @param $keys Array |
| 145 | + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
| 146 | + * @return bool Resources able to be locked |
| 147 | + */ |
| 148 | + protected function doLockingRequest( $lockSrv, array $keys, $type ) { |
| 149 | + if ( $type == self::LOCK_SH ) { // reader locks |
| 150 | + $type = 'SH'; |
| 151 | + } elseif ( $type == self::LOCK_EX ) { // writer locks |
| 152 | + $type = 'EX'; |
| 153 | + } else { |
| 154 | + return true; // ok... |
| 155 | + } |
| 156 | + |
| 157 | + // Send out the command and get the response... |
| 158 | + $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys ); |
| 159 | + |
| 160 | + return ( $response === 'ACQUIRED' ); |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Send a command and get back the response |
| 165 | + * |
| 166 | + * @param $lockSrv string |
| 167 | + * @param $action string |
| 168 | + * @param $type string |
| 169 | + * @param $values Array |
| 170 | + * @return string|false |
| 171 | + */ |
| 172 | + protected function sendCommand( $lockSrv, $action, $type, $values ) { |
| 173 | + $conn = $this->getConnection( $lockSrv ); |
| 174 | + if ( !$conn ) { |
| 175 | + return false; // no connection |
| 176 | + } |
| 177 | + $authKey = $this->lockServers[$lockSrv]['authKey']; |
| 178 | + // Build of the command as a flat string... |
| 179 | + $values = implode( '|', $values ); |
| 180 | + $key = sha1( $this->session . $action . $type . $values . $authKey ); |
| 181 | + // Send out the command... |
| 182 | + if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) { |
| 183 | + return false; |
| 184 | + } |
| 185 | + // Get the response... |
| 186 | + $response = fgets( $conn ); |
| 187 | + if ( $response === false ) { |
| 188 | + return false; |
| 189 | + } |
| 190 | + return trim( $response ); |
| 191 | + } |
| 192 | + |
| 193 | + /** |
| 194 | + * Attempt to acquire locks with the peers for a bucket. |
| 195 | + * |
| 196 | + * @param $bucket integer |
| 197 | + * @param $keys Array List of resource keys to lock |
| 198 | + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
| 199 | + * @return bool|string One of (true, 'cantacquire', 'srverrors') |
| 200 | + */ |
| 201 | + protected function doLockingRequestAll( $bucket, array $keys, $type ) { |
| 202 | + $yesVotes = 0; // locks made on trustable servers |
| 203 | + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers |
| 204 | + $quorum = floor( $votesLeft/2 + 1 ); // simple majority |
| 205 | + // Get votes for each peer, in order, until we have enough... |
| 206 | + foreach ( $this->srvsByBucket[$bucket] as $index => $lockSrv ) { |
| 207 | + // Attempt to acquire the lock on this peer |
| 208 | + if ( !$this->doLockingRequest( $lockSrv, $keys, $type ) ) { |
| 209 | + return 'cantacquire'; // vetoed; resource locked |
| 210 | + } |
| 211 | + ++$yesVotes; // success for this peer |
| 212 | + if ( $yesVotes >= $quorum ) { |
| 213 | + return true; // lock obtained |
| 214 | + } |
| 215 | + $votesLeft--; |
| 216 | + $votesNeeded = $quorum - $yesVotes; |
| 217 | + if ( $votesNeeded > $votesLeft ) { |
| 218 | + // In "trust cache" mode we don't have to meet the quorum |
| 219 | + break; // short-circuit |
| 220 | + } |
| 221 | + } |
| 222 | + // At this point, we must not have meet the quorum |
| 223 | + return 'srverrors'; // not enough votes to ensure correctness |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Get (or reuse) a connection to a lock server |
| 228 | + * |
| 229 | + * @param $lockSrv string |
| 230 | + * @return resource |
| 231 | + */ |
| 232 | + protected function getConnection( $lockSrv ) { |
| 233 | + if ( !isset( $this->conns[$lockSrv] ) ) { |
| 234 | + $cfg = $this->lockServers[$lockSrv]; |
| 235 | + wfSuppressWarnings(); |
| 236 | + $errno = $errstr = ''; |
| 237 | + $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout ); |
| 238 | + wfRestoreWarnings(); |
| 239 | + if ( $conn === false ) { |
| 240 | + return null; |
| 241 | + } |
| 242 | + $sec = floor( $this->connTimeout ); |
| 243 | + $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 ); |
| 244 | + stream_set_timeout( $conn, $sec, $usec ); |
| 245 | + $this->conns[$lockSrv] = $conn; |
| 246 | + } |
| 247 | + return $this->conns[$lockSrv]; |
| 248 | + } |
| 249 | + |
| 250 | + /** |
| 251 | + * Release all locks that this session is holding. |
| 252 | + * |
| 253 | + * @return Status |
| 254 | + */ |
| 255 | + protected function releaseLocks() { |
| 256 | + $status = Status::newGood(); |
| 257 | + foreach ( $this->conns as $lockSrv => $conn ) { |
| 258 | + $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() ); |
| 259 | + if ( $response !== 'RELEASED_ALL' ) { |
| 260 | + $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); |
| 261 | + } |
| 262 | + } |
| 263 | + return $status; |
| 264 | + } |
| 265 | + |
| 266 | + /** |
| 267 | + * Get the bucket for lock key. |
| 268 | + * |
| 269 | + * @param $key string (40 char hex key) |
| 270 | + * @return integer |
| 271 | + */ |
| 272 | + protected function getBucketFromKey( $key ) { |
| 273 | + $prefix = substr( $key, 0, 2 ); // first 2 hex chars (8 bits) |
| 274 | + return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->srvsByBucket ); |
| 275 | + } |
| 276 | + |
| 277 | + /** |
| 278 | + * Make sure remaining locks get cleared for sanity |
| 279 | + */ |
| 280 | + function __destruct() { |
| 281 | + $this->releaseLocks(); |
| 282 | + foreach ( $this->conns as $lockSrv => $conn ) { |
| 283 | + fclose( $conn ); |
| 284 | + } |
| 285 | + } |
| 286 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LSLockManager.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 287 | + native |
Index: branches/FileBackend/phase3/includes/AutoLoader.php |
— | — | @@ -496,6 +496,7 @@ |
497 | 497 | 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php', |
498 | 498 | 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php', |
499 | 499 | 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php', |
| 500 | + 'LSLockManager' => 'includes/filerepo/backend/lockmanager/LSLockManager.php', |
500 | 501 | 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php', |
501 | 502 | 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', |
502 | 503 | 'FileOp' => 'includes/filerepo/backend/FileOp.php', |
Index: branches/FileBackend/phase3/languages/messages/MessagesEn.php |
— | — | @@ -2279,8 +2279,9 @@ |
2280 | 2280 | 'lockmanager-fail-acquirelock' => 'Could not acquire lock for key "$1".', |
2281 | 2281 | 'lockmanager-fail-releaselock' => 'Could not release lock for key "$1".', |
2282 | 2282 | 'lockmanager-fail-acquirelocks' => 'Could not acquire locks for keys "$1".', |
2283 | | -'lockmanager-fail-db-bucket' => 'Could not contact enough lock servers in bucket $1', |
2284 | | -'lockmanager-fail-db-release' => 'Could not release locks on server $1', |
| 2283 | +'lockmanager-fail-db-bucket' => 'Could not contact enough lock databases in bucket $1', |
| 2284 | +'lockmanager-fail-db-release' => 'Could not release locks on database $1', |
| 2285 | +'lockmanager-fail-svr-release' => 'Could not release locks on server $1', |
2285 | 2286 | |
2286 | 2287 | # img_auth script messages |
2287 | 2288 | 'img-auth-accessdenied' => 'Access denied', |