diff --git a/.gitignore b/.gitignore index 8af0104..02b1bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /vendor/ /.vscode/ /.ppm/ -composer.lock \ No newline at end of file +/web/ +composer.lock +.phpunit.result.cache diff --git a/README.md b/README.md index 6796bee..45b4363 100644 --- a/README.md +++ b/README.md @@ -178,3 +178,7 @@ framework: ``` More information at http://symfony.com/doc/current/cookbook/request/load_balancer_reverse_proxy.html. + +### Programmatically restarting Worker + +We provide the `X-PPM-Restart` HTTP Header to restart the current worker with content `worker` or `all`. You can send this header in the response from your application. \ No newline at end of file diff --git a/composer.json b/composer.json index c2fa807..97a94a9 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,15 @@ { "name": "php-pm/php-pm", - "description": "PHP-PM is a process manager, supercharger and load balancer for PHP applications.", + "description": "PHP-PM is a process manager, supercharger and load balancer for PHP applications. | This is a fork that we will try to keep up to date with the upstream.", "license": "MIT", "require": { - "php": "^7.3 || ^8.0 ", + "php": "^7.4 || ^8.0 ", "ext-pcntl": "*", - "symfony/console": "^3.4|^4|^5", - "symfony/error-handler": "^4.4|^5", - "symfony/process": "^3.4|^4|^5", + "symfony/console": "^4|^5|^6", + "symfony/error-handler": "^4.4|^5|^6", + "symfony/process": "^4|^5|^6", "react/event-loop": "^1.0", - "react/http": ">=1.0 <1.3", + "react/http": "^1.0", "react/stream": "^1.0", "react/socket": "^1.0", "react/child-process": "^0.6", diff --git a/src/RequestHandler.php b/src/RequestHandler.php index 963683f..bbe0282 100644 --- a/src/RequestHandler.php +++ b/src/RequestHandler.php @@ -74,6 +74,13 @@ class RequestHandler */ private $slave; + /** + * Contains the content from 'X-PPM-Restart' + * + * @var string + */ + private $restartMode; + private $redirectionTries = 0; private $incomingBuffer = ''; private $lastOutgoingData = ''; // Used to track abnormal responses @@ -122,7 +129,8 @@ public function handleData($data) $remoteAddress = (string) $this->incoming->getRemoteAddress(); $headersToReplace = [ 'X-PHP-PM-Remote-IP' => \trim(\parse_url($remoteAddress, PHP_URL_HOST), '[]'), - 'X-PHP-PM-Remote-Port' => \trim(\parse_url($remoteAddress, PHP_URL_PORT), '[]') + 'X-PHP-PM-Remote-Port' => \trim(\parse_url($remoteAddress, PHP_URL_PORT), '[]'), + 'Connection' => 'close' ]; $buffer = $this->replaceHeader($this->incomingBuffer, $headersToReplace); @@ -157,10 +165,11 @@ public function getNextSlave() // keep retrying until slave becomes available, unless timeout has been exceeded if (\time() < ($this->requestSentAt + $this->timeout)) { - $this->loop->futureTick([$this, 'getNextSlave']); + // add a small delay to avoid busy waiting + $this->loop->addTimer(.01, [$this, 'getNextSlave']); } else { // Return a "503 Service Unavailable" response - $this->output->writeln(\sprintf('No slaves available to handle the request and timeout %d seconds exceeded', $this->timeout)); + $this->output->writeln(\sprintf('No worker processes available to handle the request and timeout %d seconds exceeded', $this->timeout)); $this->incoming->write($this->createErrorResponse('503 Service Temporarily Unavailable', 'Service Temporarily Unavailable')); $this->incoming->end(); } @@ -169,12 +178,12 @@ public function getNextSlave() private function createErrorResponse($code, $text) { return \sprintf( - 'HTTP/1.1 %s'."\n". - 'Date: %s'."\n". - 'Content-Type: text/plain'."\n". - 'Content-Length: %s'."\n". - "\n". - '%s', + 'HTTP/1.1 %s' . "\n" . + 'Date: %s' . "\n" . + 'Content-Type: text/plain' . "\n" . + 'Content-Length: %s' . "\n" . + "\n" . + '%s', $code, \gmdate('D, d M Y H:i:s T'), \strlen($text), @@ -244,10 +253,22 @@ public function slaveConnected(ConnectionInterface $connection) // keep track of the last sent data to detect if slave exited abnormally $this->connection->on('data', function ($data) { $this->lastOutgoingData = $data; - }); - // relay data to client - $this->connection->pipe($this->incoming, ['end' => false]); + // relay data to client + if (stripos($data, 'X-PPM-Restart: worker') !== false) { + $this->restartMode = 'worker'; + } + + if (stripos($data, 'X-PPM-Restart: all') !== false) { + $this->restartMode = 'all'; + } + + if ($this->restartMode) { + $data = $this->removeHeader($data, 'X-PPM-Restart'); + } + + $this->incoming->write($data); + }); } /** @@ -326,6 +347,24 @@ public function slaveClosed() $this->output->writeln(\sprintf('Restart worker #%d because it reached memory limit of %d', $this->slave->getPort(), $memoryLimit)); $connection->close(); } + if ($this->restartMode === 'worker') { + $this->slave->close(); + $this->output->writeln(sprintf('Restart worker #%d because "X-PPM-Worker" Header with content "worker" was send', $this->slave->getPort())); + $connection->close(); + + $this->restartMode = ''; + } + + if ($this->restartMode === 'all') { + foreach ($this->slaves->getSlaves() as $slave) { + $slave->getConnection()->close(); + $slave->close(); + + $this->output->writeln(sprintf('Restart worker #%d because "X-PPM-Worker" Header with content "all" was send', $slave->getPort())); + } + + $this->restartMode = ''; + } } } @@ -346,7 +385,7 @@ public function slaveConnectFailed(\Exception $e) $this->verboseTimer(function ($took) use ($e) { return \sprintf( 'Connection to worker %d failed. Try #%d, took %.3fs ' . - '(timeout %ds). Error message: [%d] %s', + '(timeout %ds). Error message: [%d] %s', $this->slave->getPort(), $this->redirectionTries, $took, @@ -393,6 +432,18 @@ protected function isHeaderEnd($buffer) return false !== \strpos($buffer, "\r\n\r\n"); } + protected function removeHeader($header, $headerToRemove) + { + $result = $header; + + if (false !== $headerPosition = stripos($result, $headerToRemove . ':')) { + $length = strpos(substr($header, $headerPosition), "\r\n"); + $result = substr_replace($result, '', $headerPosition, $length); + } + + return $result; + } + /** * Replaces or injects header * diff --git a/src/SlavePool.php b/src/SlavePool.php index c4e8e99..dc2da68 100644 --- a/src/SlavePool.php +++ b/src/SlavePool.php @@ -120,4 +120,15 @@ public function getStatusSummary() return \count($this->getByStatus($state)); }, $map); } + + /** + * Returns all slaves in pool + * + * @return Slave[] + */ + + public function getSlaves() + { + return $this->slaves; + } }