Revert r106521: creates lots of long, unwrappable lines in help output
[mediawiki.git] / maintenance / locking / LockServerDaemon.php
blobcf9d948b72438decb9c4b64c7283a432cb6f729e
1 <?php
3 if ( php_sapi_name() !== 'cli' ) {
4 die( "This is not a valid entry point.\n" );
6 error_reporting( E_ALL );
8 // Run the server...
9 set_time_limit( 0 );
10 LockServerDaemon::init(
11 getopt( '', array(
12 'address:', 'port:', 'authKey:',
13 'connTimeout::', 'lockTimeout::', 'maxClients::', 'maxBacklog::', 'maxLocks::',
14 ) )
15 )->main();
17 /**
18 * Simple lock server daemon that accepts lock/unlock requests.
19 * This should not require MediaWiki setup or PHP files.
21 class LockServerDaemon {
22 /** @var resource */
23 protected $sock; // socket to listen/accept on
24 /** @var Array */
25 protected $shLocks = array(); // (key => session => 1)
26 /** @var Array */
27 protected $exLocks = array(); // (key => session)
28 /** @var Array */
29 protected $sessions = array(); // (session => resource)
30 /** @var Array */
31 protected $deadSessions = array(); // (session => UNIX timestamp)
33 /** @var Array */
34 protected $sessionIndexSh = array(); // (session => key => 1)
35 /** @var Array */
36 protected $sessionIndexEx = array(); // (session => key => 1)
38 protected $address; // string (IP/hostname)
39 protected $port; // integer
40 protected $authKey; // string key
41 protected $connTimeout; // array ( 'sec' => integer, 'usec' => integer )
42 protected $lockTimeout; // integer number of seconds
43 protected $maxLocks; // integer
44 protected $maxClients; // integer
45 protected $maxBacklog; // integer
47 protected $startTime; // integer UNIX timestamp
48 protected $lockCount = 0; // integer
49 protected $ticks = 0; // integer counter
51 protected static $instance = null;
53 /**
54 * @params $config Array
55 * @return LockServerDaemon
57 public function init( array $config ) {
58 if ( self::$instance ) {
59 throw new Exception( 'LockServer already initialized.' );
61 self::$instance = new self( $config );
62 return self::$instance;
65 /**
66 * @params $config Array
68 protected function __construct( array $config ) {
69 $required = array( 'address', 'port', 'authKey' );
70 foreach ( $required as $par ) {
71 if ( !isset( $config[$par] ) ) {
72 throw new Exception( "Parameter '$par' must be specified." );
76 $this->address = $config['address'];
77 $this->port = $config['port'];
78 $this->authKey = $config['authKey'];
80 $connTimeout = isset( $config['connTimeout'] )
81 ? $config['connTimeout']
82 : 1.5;
83 $this->connTimeout = array(
84 'sec' => floor( $connTimeout ),
85 'usec' => floor( ( $connTimeout - floor( $connTimeout ) ) * 1e6 )
87 $this->lockTimeout = isset( $config['lockTimeout'] )
88 ? $config['lockTimeout']
89 : 60;
90 $this->maxLocks = isset( $config['maxLocks'] )
91 ? $config['maxLocks']
92 : 5000;
93 $this->maxClients = isset( $config['maxClients'] )
94 ? $config['maxClients']
95 : 100;
96 $this->maxBacklog = isset( $config['maxBacklog'] )
97 ? $config['maxBacklog']
98 : 10;
102 * @return void
104 protected function setupSocket() {
105 if ( !function_exists( 'socket_create' ) ) {
106 throw new Exception( "PHP sockets extension missing from PHP CLI mode." );
108 $sock = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
109 if ( $sock === false ) {
110 throw new Exception( "socket_create(): " . socket_strerror( socket_last_error() ) );
112 socket_set_option( $sock, SOL_SOCKET, SO_REUSEADDR, 1 ); // bypass 2MLS
113 if ( socket_bind( $sock, $this->address, $this->port ) === false ) {
114 throw new Exception( "socket_bind(): " .
115 socket_strerror( socket_last_error( $sock ) ) );
116 } elseif ( socket_listen( $sock, $this->maxBacklog ) === false ) {
117 throw new Exception( "socket_listen(): " .
118 socket_strerror( socket_last_error( $sock ) ) );
120 $this->sock = $sock;
122 $this->startTime = time();
126 * @return void
128 public function main() {
129 // Setup socket and start listing
130 $this->setupSocket();
131 // Create a list of all the clients that will be connected to us.
132 $clients = array( $this->sock ); // start off with listening socket
133 do {
134 // Create a copy, so $clients doesn't get modified by socket_select()
135 $read = $clients; // clients-with-data
136 // Get a list of all the clients that have data to be read from
137 $changed = socket_select( $read, $write = NULL, $except = NULL, NULL );
138 if ( $changed === false ) {
139 trigger_error( 'socket_listen(): ' . socket_strerror( socket_last_error() ) );
140 continue;
141 } elseif ( $changed < 1 ) {
142 continue; // wait
144 // Check if there is a client trying to connect...
145 if ( in_array( $this->sock, $read ) && count( $clients ) < $this->maxClients ) {
146 // Accept the new client...
147 $newsock = socket_accept( $this->sock );
148 socket_set_option( $newsock, SOL_SOCKET, SO_RCVTIMEO, $this->connTimeout );
149 socket_set_option( $newsock, SOL_SOCKET, SO_SNDTIMEO, $this->connTimeout );
150 $clients[] = $newsock;
151 // Remove the listening socket from the clients-with-data array...
152 $key = array_search( $this->sock, $read );
153 unset( $read[$key] );
155 // Loop through all the clients that have data to read...
156 foreach ( $read as $read_sock ) {
157 // Read until newline or 65535 bytes are recieved.
158 // socket_read show errors when the client is disconnected.
159 $data = @socket_read( $read_sock, 65535, PHP_NORMAL_READ );
160 // Check if the client is disconnected
161 if ( $data === false ) {
162 // Remove client from $clients list
163 $key = array_search( $read_sock, $clients );
164 unset( $clients[$key] );
165 // Remove socket's session from tracking (if it exists)
166 $session = array_search( $read_sock, $this->sessions );
167 if ( $session !== false ) {
168 unset( $this->sessions[$session] );
169 // Record recently killed sessions that still have locks
170 if ( isset( $this->sessionIndexSh[$session] )
171 || isset( $this->sessionIndexEx[$session] ) )
173 $this->deadSessions[$session] = time();
176 } else {
177 // Perform the requested command...
178 $response = $this->doCommand( trim( $data ), $read_sock );
179 // Send the response to the client...
180 if ( socket_write( $read_sock, "$response\n" ) === false ) {
181 trigger_error( 'socket_write(): ' .
182 socket_strerror( socket_last_error( $read_sock ) ) );
186 // Prune dead locks every 10 socket events...
187 if ( ++$this->ticks >= 9 ) {
188 $this->ticks = 0;
189 $this->purgeExpiredLocks();
191 } while ( true );
195 * @param $data string
196 * @param $sourceSock resource
197 * @return string
199 protected function doCommand( $data, $sourceSock ) {
200 $cmdArr = $this->getCommand( $data );
201 if ( is_string( $cmdArr ) ) {
202 return $cmdArr; // error
204 list( $function, $session, $type, $resources ) = $cmdArr;
205 // On first command, track the session => sock correspondence
206 if ( !isset( $this->sessions[$session] ) ) {
207 $this->sessions[$session] = $sourceSock;
209 if ( $function === 'ACQUIRE' ) {
210 return $this->lock( $session, $type, $resources );
211 } elseif ( $function === 'RELEASE' ) {
212 return $this->unlock( $session, $type, $resources );
213 } elseif ( $function === 'RELEASE_ALL' ) {
214 return $this->release( $session );
215 } elseif ( $function === 'STAT' ) {
216 return $this->stat();
218 return 'INTERNAL_ERROR';
222 * @param $data string
223 * @return Array
225 protected function getCommand( $data ) {
226 $m = explode( ':', $data ); // <session, key, command, type, values>
227 if ( count( $m ) == 5 ) {
228 list( $session, $key, $command, $type, $values ) = $m;
229 if ( sha1( $session . $command . $type . $values . $this->authKey ) !== $key ) {
230 return 'BAD_KEY';
231 } elseif ( strlen( $session ) !== 31 ) {
232 return 'BAD_SESSION';
234 $values = explode( '|', $values );
235 if ( $command === 'ACQUIRE' ) {
236 $needsLockArgs = true;
237 } elseif ( $command === 'RELEASE' ) {
238 $needsLockArgs = true;
239 } elseif ( $command === 'RELEASE_ALL' ) {
240 $needsLockArgs = false;
241 } elseif ( $command === 'STAT' ) {
242 $needsLockArgs = false;
243 } else {
244 return 'BAD_COMMAND';
246 if ( $needsLockArgs ) {
247 if ( $type !== 'SH' && $type !== 'EX' ) {
248 return 'BAD_TYPE';
250 foreach ( $values as $value ) {
251 if ( strlen( $value ) !== 31 ) {
252 return 'BAD_FORMAT';
256 return array( $command, $session, $type, $values );
258 return 'BAD_FORMAT';
262 * @param $session string
263 * @param $type string
264 * @param $keys Array
265 * @return string
267 protected function lock( $session, $type, $keys ) {
268 if ( $this->lockCount >= $this->maxLocks ) {
269 return 'TOO_MANY_LOCKS';
271 if ( $type === 'SH' ) {
272 // Check if any keys are already write-locked...
273 foreach ( $keys as $key ) {
274 if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] !== $session ) {
275 return 'CANT_ACQUIRE';
278 // Acquire the read-locks...
279 foreach ( $keys as $key ) {
280 $this->set_sh_lock( $key, $session );
282 return 'ACQUIRED';
283 } elseif ( $type === 'EX' ) {
284 // Check if any keys are already read-locked or write-locked...
285 foreach ( $keys as $key ) {
286 if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] !== $session ) {
287 return 'CANT_ACQUIRE';
289 if ( isset( $this->shLocks[$key] ) ) {
290 foreach ( $this->shLocks[$key] as $otherSession => $x ) {
291 if ( $otherSession !== $session ) {
292 return 'CANT_ACQUIRE';
297 // Acquire the write-locks...
298 foreach ( $keys as $key ) {
299 $this->set_ex_lock( $key, $session );
301 return 'ACQUIRED';
303 return 'INTERNAL_ERROR';
307 * @param $session string
308 * @param $type string
309 * @param $keys Array
310 * @return string
312 protected function unlock( $session, $type, $keys ) {
313 if ( $type === 'SH' ) {
314 foreach ( $keys as $key ) {
315 $this->unset_sh_lock( $key, $session );
317 return 'RELEASED';
318 } elseif ( $type === 'EX' ) {
319 foreach ( $keys as $key ) {
320 $this->unset_ex_lock( $key, $session );
322 return 'RELEASED';
324 return 'INTERNAL_ERROR';
328 * @param $session string
329 * @return string
331 protected function release( $session ) {
332 if ( isset( $this->sessionIndexSh[$session] ) ) {
333 foreach ( $this->sessionIndexSh[$session] as $key => $x ) {
334 $this->unset_sh_lock( $key, $session );
337 if ( isset( $this->sessionIndexEx[$session] ) ) {
338 foreach ( $this->sessionIndexEx[$session] as $key => $x ) {
339 $this->unset_ex_lock( $key, $session );
342 return 'RELEASED_ALL';
346 * @return string
348 protected function stat() {
349 return ( time() - $this->startTime ) . ':' . memory_get_usage();
353 * Clear locks for sessions that have been dead for a while
355 * @return void
357 protected function purgeExpiredLocks() {
358 $now = time();
359 foreach ( $this->deadSessions as $session => $timestamp ) {
360 if ( ( $now - $timestamp ) > $this->lockTimeout ) {
361 $this->release( $session );
362 unset( $this->deadSessions[$session] );
368 * @param $key string
369 * @param $session string
370 * @return void
372 protected function set_sh_lock( $key, $session ) {
373 if ( !isset( $this->shLocks[$key][$session] ) ) {
374 $this->shLocks[$key][$session] = 1;
375 $this->sessionIndexSh[$session][$key] = 1;
376 ++$this->lockCount; // we are adding a lock
381 * @param $key string
382 * @param $session string
383 * @return void
385 protected function set_ex_lock( $key, $session ) {
386 if ( !isset( $this->exLocks[$key][$session] ) ) {
387 $this->exLocks[$key] = $session;
388 $this->sessionIndexEx[$session][$key] = 1;
389 ++$this->lockCount; // we are adding a lock
394 * @param $key string
395 * @param $session string
396 * @return void
398 protected function unset_sh_lock( $key, $session ) {
399 if ( isset( $this->shLocks[$key][$session] ) ) {
400 unset( $this->shLocks[$key][$session] );
401 if ( !count( $this->shLocks[$key] ) ) {
402 unset( $this->shLocks[$key] );
404 unset( $this->sessionIndexSh[$session][$key] );
405 if ( !count( $this->sessionIndexSh[$session] ) ) {
406 unset( $this->sessionIndexSh[$session] );
408 --$this->lockCount;
413 * @param $key string
414 * @param $session string
415 * @return void
417 protected function unset_ex_lock( $key, $session ) {
418 if ( isset( $this->exLocks[$key] ) && $this->exLocks[$key] === $session ) {
419 unset( $this->exLocks[$key] );
420 unset( $this->sessionIndexEx[$session][$key] );
421 if ( !count( $this->sessionIndexEx[$session] ) ) {
422 unset( $this->sessionIndexEx[$session] );
424 --$this->lockCount;