Merge "Remove weird, confusing, unreachable code"
[mediawiki.git] / includes / SquidPurgeClient.php
blob8eb0f6bfc850a7de96c2a7f0cbd18511761e6f50
1 <?php
2 /**
3 * Squid and Varnish cache purging.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
23 /**
24 * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
25 * Uses asynchronous I/O, allowing purges to be done in a highly parallel
26 * manner.
28 * Could be replaced by curl_multi_exec() or some such.
30 class SquidPurgeClient {
31 var $host, $port, $ip;
33 var $readState = 'idle';
34 var $writeBuffer = '';
35 var $requests = array();
36 var $currentRequestIndex;
38 const EINTR = 4;
39 const EAGAIN = 11;
40 const EINPROGRESS = 115;
41 const BUFFER_SIZE = 8192;
43 /**
44 * The socket resource, or null for unconnected, or false for disabled due to error
46 var $socket;
48 var $readBuffer;
50 var $bodyRemaining;
52 /**
53 * @param $server string
54 * @param $options array
56 public function __construct( $server, $options = array() ) {
57 $parts = explode( ':', $server, 2 );
58 $this->host = $parts[0];
59 $this->port = isset( $parts[1] ) ? $parts[1] : 80;
62 /**
63 * Open a socket if there isn't one open already, return it.
64 * Returns false on error.
66 * @return bool|resource
68 protected function getSocket() {
69 if ( $this->socket !== null ) {
70 return $this->socket;
73 $ip = $this->getIP();
74 if ( !$ip ) {
75 $this->log( "DNS error" );
76 $this->markDown();
77 return false;
79 $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
80 socket_set_nonblock( $this->socket );
81 wfSuppressWarnings();
82 $ok = socket_connect( $this->socket, $ip, $this->port );
83 wfRestoreWarnings();
84 if ( !$ok ) {
85 $error = socket_last_error( $this->socket );
86 if ( $error !== self::EINPROGRESS ) {
87 $this->log( "connection error: " . socket_strerror( $error ) );
88 $this->markDown();
89 return false;
93 return $this->socket;
96 /**
97 * Get read socket array for select()
98 * @return array
100 public function getReadSocketsForSelect() {
101 if ( $this->readState == 'idle' ) {
102 return array();
104 $socket = $this->getSocket();
105 if ( $socket === false ) {
106 return array();
108 return array( $socket );
112 * Get write socket array for select()
113 * @return array
115 public function getWriteSocketsForSelect() {
116 if ( !strlen( $this->writeBuffer ) ) {
117 return array();
119 $socket = $this->getSocket();
120 if ( $socket === false ) {
121 return array();
123 return array( $socket );
126 /**
127 * Get the host's IP address.
128 * Does not support IPv6 at present due to the lack of a convenient interface in PHP.
130 protected function getIP() {
131 if ( $this->ip === null ) {
132 if ( IP::isIPv4( $this->host ) ) {
133 $this->ip = $this->host;
134 } elseif ( IP::isIPv6( $this->host ) ) {
135 throw new MWException( '$wgSquidServers does not support IPv6' );
136 } else {
137 wfSuppressWarnings();
138 $this->ip = gethostbyname( $this->host );
139 if ( $this->ip === $this->host ) {
140 $this->ip = false;
142 wfRestoreWarnings();
145 return $this->ip;
149 * Close the socket and ignore any future purge requests.
150 * This is called if there is a protocol error.
152 protected function markDown() {
153 $this->close();
154 $this->socket = false;
158 * Close the socket but allow it to be reopened for future purge requests
160 public function close() {
161 if ( $this->socket ) {
162 wfSuppressWarnings();
163 socket_set_block( $this->socket );
164 socket_shutdown( $this->socket );
165 socket_close( $this->socket );
166 wfRestoreWarnings();
168 $this->socket = null;
169 $this->readBuffer = '';
170 // Write buffer is kept since it may contain a request for the next socket
174 * Queue a purge operation
176 * @param $url string
178 public function queuePurge( $url ) {
179 $url = SquidUpdate::expand( str_replace( "\n", '', $url ) );
180 $this->requests[] = "PURGE $url HTTP/1.0\r\n" .
181 "Connection: Keep-Alive\r\n" .
182 "Proxy-Connection: Keep-Alive\r\n" .
183 "User-Agent: " . Http::userAgent() . ' ' . __CLASS__ . "\r\n\r\n";
184 if ( $this->currentRequestIndex === null ) {
185 $this->nextRequest();
190 * @return bool
192 public function isIdle() {
193 return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
197 * Perform pending writes. Call this when socket_select() indicates that writing will not block.
199 public function doWrites() {
200 if ( !strlen( $this->writeBuffer ) ) {
201 return;
203 $socket = $this->getSocket();
204 if ( !$socket ) {
205 return;
208 if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
209 $buf = $this->writeBuffer;
210 $flags = MSG_EOR;
211 } else {
212 $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
213 $flags = 0;
215 wfSuppressWarnings();
216 $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
217 wfRestoreWarnings();
219 if ( $bytesSent === false ) {
220 $error = socket_last_error( $socket );
221 if ( $error != self::EAGAIN && $error != self::EINTR ) {
222 $this->log( 'write error: ' . socket_strerror( $error ) );
223 $this->markDown();
225 return;
228 $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
232 * Read some data. Call this when socket_select() indicates that the read buffer is non-empty.
234 public function doReads() {
235 $socket = $this->getSocket();
236 if ( !$socket ) {
237 return;
240 $buf = '';
241 wfSuppressWarnings();
242 $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
243 wfRestoreWarnings();
244 if ( $bytesRead === false ) {
245 $error = socket_last_error( $socket );
246 if ( $error != self::EAGAIN && $error != self::EINTR ) {
247 $this->log( 'read error: ' . socket_strerror( $error ) );
248 $this->markDown();
249 return;
251 } elseif ( $bytesRead === 0 ) {
252 // Assume EOF
253 $this->close();
254 return;
257 $this->readBuffer .= $buf;
258 while ( $this->socket && $this->processReadBuffer() === 'continue' );
262 * @throws MWException
263 * @return string
265 protected function processReadBuffer() {
266 switch ( $this->readState ) {
267 case 'idle':
268 return 'done';
269 case 'status':
270 case 'header':
271 $lines = explode( "\r\n", $this->readBuffer, 2 );
272 if ( count( $lines ) < 2 ) {
273 return 'done';
275 if ( $this->readState == 'status' ) {
276 $this->processStatusLine( $lines[0] );
277 } else { // header
278 $this->processHeaderLine( $lines[0] );
280 $this->readBuffer = $lines[1];
281 return 'continue';
282 case 'body':
283 if ( $this->bodyRemaining !== null ) {
284 if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
285 $this->bodyRemaining -= strlen( $this->readBuffer );
286 $this->readBuffer = '';
287 return 'done';
288 } else {
289 $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
290 $this->bodyRemaining = 0;
291 $this->nextRequest();
292 return 'continue';
294 } else {
295 // No content length, read all data to EOF
296 $this->readBuffer = '';
297 return 'done';
299 default:
300 throw new MWException( __METHOD__.': unexpected state' );
305 * @param $line
306 * @return
308 protected function processStatusLine( $line ) {
309 if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
310 $this->log( 'invalid status line' );
311 $this->markDown();
312 return;
314 list( , , , $status, $reason ) = $m;
315 $status = intval( $status );
316 if ( $status !== 200 && $status !== 404 ) {
317 $this->log( "unexpected status code: $status $reason" );
318 $this->markDown();
319 return;
321 $this->readState = 'header';
325 * @param $line string
327 protected function processHeaderLine( $line ) {
328 if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
329 $this->bodyRemaining = intval( $m[1] );
330 } elseif ( $line === '' ) {
331 $this->readState = 'body';
335 protected function nextRequest() {
336 if ( $this->currentRequestIndex !== null ) {
337 unset( $this->requests[$this->currentRequestIndex] );
339 if ( count( $this->requests ) ) {
340 $this->readState = 'status';
341 $this->currentRequestIndex = key( $this->requests );
342 $this->writeBuffer = $this->requests[$this->currentRequestIndex];
343 } else {
344 $this->readState = 'idle';
345 $this->currentRequestIndex = null;
346 $this->writeBuffer = '';
348 $this->bodyRemaining = null;
352 * @param $msg string
354 protected function log( $msg ) {
355 wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" );
359 class SquidPurgeClientPool {
362 * @var array of SquidPurgeClient
364 var $clients = array();
365 var $timeout = 5;
368 * @param $options array
370 function __construct( $options = array() ) {
371 if ( isset( $options['timeout'] ) ) {
372 $this->timeout = $options['timeout'];
377 * @param $client SquidPurgeClient
378 * @return void
380 public function addClient( $client ) {
381 $this->clients[] = $client;
384 public function run() {
385 $done = false;
386 $startTime = microtime( true );
387 while ( !$done ) {
388 $readSockets = $writeSockets = array();
390 * @var $client SquidPurgeClient
392 foreach ( $this->clients as $clientIndex => $client ) {
393 $sockets = $client->getReadSocketsForSelect();
394 foreach ( $sockets as $i => $socket ) {
395 $readSockets["$clientIndex/$i"] = $socket;
397 $sockets = $client->getWriteSocketsForSelect();
398 foreach ( $sockets as $i => $socket ) {
399 $writeSockets["$clientIndex/$i"] = $socket;
402 if ( !count( $readSockets ) && !count( $writeSockets ) ) {
403 break;
405 $exceptSockets = null;
406 $timeout = min( $startTime + $this->timeout - microtime( true ), 1 );
407 wfSuppressWarnings();
408 $numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout );
409 wfRestoreWarnings();
410 if ( $numReady === false ) {
411 wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' .
412 socket_strerror( socket_last_error() ) . "\n" );
413 break;
415 // Check for timeout, use 1% tolerance since we aimed at having socket_select()
416 // exit at precisely the overall timeout
417 if ( microtime( true ) - $startTime > $this->timeout * 0.99 ) {
418 wfDebugLog( 'squid', __CLASS__.": timeout ({$this->timeout}s)\n" );
419 break;
420 } elseif ( !$numReady ) {
421 continue;
424 foreach ( $readSockets as $key => $socket ) {
425 list( $clientIndex, ) = explode( '/', $key );
426 $client = $this->clients[$clientIndex];
427 $client->doReads();
429 foreach ( $writeSockets as $key => $socket ) {
430 list( $clientIndex, ) = explode( '/', $key );
431 $client = $this->clients[$clientIndex];
432 $client->doWrites();
435 $done = true;
436 foreach ( $this->clients as $client ) {
437 if ( !$client->isIdle() ) {
438 $done = false;
442 foreach ( $this->clients as $client ) {
443 $client->close();