Permalink
Please sign in to comment.
Showing
with
613 additions
and 0 deletions.
- +9 −0 Bridges/BridgeInterface.php
- +36 −0 Bridges/Symfony.php
- +67 −0 Client.php
- +41 −0 Commands/StartCommand.php
- +34 −0 Commands/StatusCommand.php
- +269 −0 ProcessManager.php
- +119 −0 ProcessSlave.php
- +12 −0 README.md
- +15 −0 bin/ppm
- +11 −0 composer.json
9
Bridges/BridgeInterface.php
@@ -0,0 +1,9 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM\Bridges; | ||
+ | ||
+interface BridgeInterface | ||
+{ | ||
+ public function bootstrap(); | ||
+ public function onRequest(\React\Http\Request $request, \React\Http\Response $response); | ||
+} |
36
Bridges/Symfony.php
@@ -0,0 +1,36 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM\Bridges; | ||
+ | ||
+use PHPPM\Bridges\BridgeInterface; | ||
+ | ||
+class Symfony implements BridgeInterface | ||
+{ | ||
+ | ||
+ /** | ||
+ * @var \AppKernel | ||
+ */ | ||
+ protected $kernel; | ||
+ | ||
+ public function bootstrap() | ||
+ { | ||
+ require_once './vendor/autoload.php'; | ||
+ require_once './app/AppKernel.php'; | ||
+ $this->kernel = new \AppKernel('prod', false); | ||
+ $this->kernel->loadClassCache(); | ||
+ } | ||
+ | ||
+ public function onRequest(\React\Http\Request $request, \React\Http\Response $response) | ||
+ { | ||
+ $syRequest = new \Symfony\Component\HttpFoundation\Request(); | ||
+ $syRequest->headers->replace($request->getHeaders()); | ||
+ $syRequest->setMethod($request->getMethod()); | ||
+ $syRequest->server->set('REQUEST_URI', $request->getPath()); | ||
+ $syRequest->server->set('SERVER_NAME', explode(':', $request->getHeaders()['Host'])[0]); | ||
+ | ||
+ $syResponse = $this->kernel->handle($syRequest); | ||
+ $headers = array_map('current', $syResponse->headers->all()); | ||
+ $response->writeHead($syResponse->getStatusCode(), $headers); | ||
+ $response->end($syResponse->getContent()); | ||
+ } | ||
+} |
67
Client.php
@@ -0,0 +1,67 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM; | ||
+ | ||
+class Client | ||
+{ | ||
+ /** | ||
+ * @var int | ||
+ */ | ||
+ protected $controllerPort = 5100; | ||
+ | ||
+ /** | ||
+ * @var \React\EventLoop\ExtEventLoop|\React\EventLoop\LibEventLoop|\React\EventLoop\LibEvLoop|\React\EventLoop\StreamSelectLoop | ||
+ */ | ||
+ protected $loop; | ||
+ | ||
+ /** | ||
+ * @var \React\Socket\Connection | ||
+ */ | ||
+ protected $connection; | ||
+ | ||
+ function __construct($controllerPort = 8080) | ||
+ { | ||
+ $this->controllerPort = $controllerPort; | ||
+ $this->loop = \React\EventLoop\Factory::create(); | ||
+ } | ||
+ | ||
+ /** | ||
+ * @return \React\Socket\Connection | ||
+ */ | ||
+ protected function getConnection() | ||
+ { | ||
+ if ($this->connection) { | ||
+ $this->connection->close(); | ||
+ unset($this->connection); | ||
+ } | ||
+ $client = stream_socket_client('tcp://127.0.0.1:5500'); | ||
+ $this->connection = new \React\Socket\Connection($client, $this->loop); | ||
+ return $this->connection; | ||
+ } | ||
+ | ||
+ protected function request($command, $options, $callback) | ||
+ { | ||
+ $data['cmd'] = $command; | ||
+ $data['options'] = $options; | ||
+ $connection = $this->getConnection(); | ||
+ | ||
+ $result = ''; | ||
+ $connection->on('data', function($data) use ($result) { | ||
+ $result .= $data; | ||
+ }); | ||
+ | ||
+ $connection->on('close', function() use ($callback, $result) { | ||
+ $callback($result); | ||
+ }); | ||
+ | ||
+ $connection->write(json_encode($data)); | ||
+ } | ||
+ | ||
+ public function getStatus(callable $callback) | ||
+ { | ||
+ $this->request('status', [], function($result) use ($callback) { | ||
+ $callback(json_decode($result, true)); | ||
+ }); | ||
+ } | ||
+ | ||
+} |
41
Commands/StartCommand.php
@@ -0,0 +1,41 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM\Commands; | ||
+ | ||
+use PHPPM\ProcessManager; | ||
+use Symfony\Component\Console\Command\Command; | ||
+use Symfony\Component\Console\Input\InputInterface; | ||
+use Symfony\Component\Console\Input\InputOption; | ||
+use Symfony\Component\Console\Output\OutputInterface; | ||
+ | ||
+class StartCommand extends Command | ||
+{ | ||
+ /** | ||
+ * {@inheritdoc} | ||
+ */ | ||
+ protected function configure() | ||
+ { | ||
+ parent::configure(); | ||
+ | ||
+ $this | ||
+ ->setName('start') | ||
+ ->addArgument('working-directory', null, 'working directory', './') | ||
+ ->addOption('bridge', null, InputOption::VALUE_REQUIRED) | ||
+ ->setDescription('Starts the server') | ||
+ ; | ||
+ } | ||
+ | ||
+ protected function execute(InputInterface $input, OutputInterface $output) | ||
+ { | ||
+ if ($workingDir = $input->getArgument('working-directory')) { | ||
+ chdir($workingDir); | ||
+ } | ||
+ | ||
+ $bridge = $input->getOption('bridge'); | ||
+ | ||
+ $handler = new ProcessManager(); | ||
+ $handler->setBridge($bridge); | ||
+ $handler->run(); | ||
+ } | ||
+ | ||
+} |
34
Commands/StatusCommand.php
@@ -0,0 +1,34 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM\Commands; | ||
+ | ||
+use PHPPM\Client; | ||
+use Symfony\Component\Console\Command\Command; | ||
+use Symfony\Component\Console\Input\InputInterface; | ||
+use Symfony\Component\Console\Output\OutputInterface; | ||
+ | ||
+class StatusCommand extends Command | ||
+{ | ||
+ /** | ||
+ * {@inheritdoc} | ||
+ */ | ||
+ protected function configure() | ||
+ { | ||
+ parent::configure(); | ||
+ | ||
+ $this | ||
+ ->setName('status') | ||
+ ->addArgument('working-directory', null, 'working directory', './') | ||
+ ->setDescription('Status of all processes') | ||
+ ; | ||
+ } | ||
+ | ||
+ protected function execute(InputInterface $input, OutputInterface $output) | ||
+ { | ||
+ $handler = new Client(); | ||
+ $handler->getStatus(function($status) { | ||
+ var_dump($status); | ||
+ }); | ||
+ } | ||
+ | ||
+} |
269
ProcessManager.php
@@ -0,0 +1,269 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM; | ||
+ | ||
+class ProcessManager | ||
+{ | ||
+ /** | ||
+ * @var array | ||
+ */ | ||
+ protected $slaves = []; | ||
+ | ||
+ /** | ||
+ * @var \React\EventLoop\LibEventLoop|\React\EventLoop\StreamSelectLoop | ||
+ */ | ||
+ protected $loop; | ||
+ | ||
+ /** | ||
+ * @var \React\Socket\Server | ||
+ */ | ||
+ protected $controller; | ||
+ | ||
+ /** | ||
+ * @var \React\Socket\Server | ||
+ */ | ||
+ protected $web; | ||
+ | ||
+ /** | ||
+ * @var int | ||
+ */ | ||
+ protected $slaveCount = 1; | ||
+ | ||
+ /** | ||
+ * @var bool | ||
+ */ | ||
+ protected $waitForSlaves = true; | ||
+ | ||
+ /** | ||
+ * Whether the server is up and thus creates new slaves when they die or not. | ||
+ * | ||
+ * @var bool | ||
+ */ | ||
+ protected $run = false; | ||
+ | ||
+ /** | ||
+ * @var int | ||
+ */ | ||
+ protected $index = 0; | ||
+ | ||
+ /** | ||
+ * @var string | ||
+ */ | ||
+ protected $bridge; | ||
+ | ||
+ /** | ||
+ * @var int | ||
+ */ | ||
+ protected $port = 8080; | ||
+ | ||
+ function __construct($port = 8080, $slaveCount = 8) | ||
+ { | ||
+ $this->slaveCount = $slaveCount; | ||
+ $this->port = $port; | ||
+ } | ||
+ | ||
+ public function fork() | ||
+ { | ||
+ if ($this->run) { | ||
+ throw new \LogicException('Can not fork when already run.'); | ||
+ } | ||
+ | ||
+ if (!pcntl_fork()) { | ||
+ $this->run(); | ||
+ } else { | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * @param string $bridge | ||
+ */ | ||
+ public function setBridge($bridge) | ||
+ { | ||
+ $this->bridge = $bridge; | ||
+ } | ||
+ | ||
+ /** | ||
+ * @return string | ||
+ */ | ||
+ public function getBridge() | ||
+ { | ||
+ return $this->bridge; | ||
+ } | ||
+ | ||
+ public function run() | ||
+ { | ||
+ $this->loop = \React\EventLoop\Factory::create(); | ||
+ $this->controller = new \React\Socket\Server($this->loop); | ||
+ $this->controller->on('connection', array($this, 'onSlaveConnection')); | ||
+ $this->controller->listen(5500); | ||
+ | ||
+ $this->web = new \React\Socket\Server($this->loop); | ||
+ $this->web->on('connection', array($this, 'onWeb')); | ||
+ $this->web->listen($this->port); | ||
+ | ||
+ echo "Waiting for slaves ... "; | ||
+ | ||
+ for ($i = 0; $i < $this->slaveCount; $i++) { | ||
+ $this->newInstance(); | ||
+ } | ||
+ | ||
+ $this->run = true; | ||
+ $this->loop(); | ||
+ } | ||
+ | ||
+ public function onWeb(\React\Socket\Connection $incoming) | ||
+ { | ||
+ $slaveId = $this->getNextSlave(); | ||
+ echo sprintf("Slave #%d, fly and win!\n", $slaveId); | ||
+ $port = $this->slaves[$slaveId]['port']; | ||
+ $client = stream_socket_client('tcp://127.0.0.1:' . $port); | ||
+ $redirect = new \React\Stream\Stream($client, $this->loop); | ||
+ | ||
+ $redirect->on( | ||
+ 'close', | ||
+ function () use ($incoming) { | ||
+ $incoming->end(); | ||
+ } | ||
+ ); | ||
+ | ||
+ $incoming->on( | ||
+ 'data', | ||
+ function ($data) use ($redirect) { | ||
+ $redirect->write($data); | ||
+ } | ||
+ ); | ||
+ | ||
+ $redirect->on( | ||
+ 'data', | ||
+ function ($data) use ($incoming) { | ||
+ $incoming->write($data); | ||
+ } | ||
+ ); | ||
+ } | ||
+ | ||
+ /** | ||
+ * @return integer | ||
+ */ | ||
+ protected function getNextSlave() | ||
+ { | ||
+ $count = count($this->slaves); | ||
+ | ||
+ $this->index++; | ||
+ if ($count === $this->index) { | ||
+ //end | ||
+ $this->index = 0; | ||
+ } | ||
+ | ||
+ return $this->index; | ||
+ } | ||
+ | ||
+ public function onSlaveConnection(\React\Socket\Connection $conn) | ||
+ { | ||
+ $conn->on( | ||
+ 'data', | ||
+ \Closure::bind( | ||
+ function ($data) use ($conn) { | ||
+ $this->onData($data, $conn); | ||
+ }, | ||
+ $this | ||
+ ) | ||
+ ); | ||
+ $conn->on( | ||
+ 'close', | ||
+ \Closure::bind( | ||
+ function () use ($conn) { | ||
+ foreach ($this->slaves as $idx => $slave) { | ||
+ if ($slave['connection'] === $conn) { | ||
+ unset($this->slaves[$idx]); | ||
+ $this->checkSlaves(); | ||
+ } | ||
+ } | ||
+ }, | ||
+ $this | ||
+ ) | ||
+ ); | ||
+ } | ||
+ | ||
+ public function onData($data, $conn) | ||
+ { | ||
+ $this->processMessage($data, $conn); | ||
+ } | ||
+ | ||
+ public function processMessage($data, $conn) | ||
+ { | ||
+ $data = json_decode($data, true); | ||
+ | ||
+ $method = 'command' . ucfirst($data['cmd']); | ||
+ if (is_callable(array($this, $method))) { | ||
+ $this->$method($data, $conn); | ||
+ } | ||
+ } | ||
+ | ||
+ protected function commandStatus($options, $conn) | ||
+ { | ||
+ $result['activeSlaves'] = count($this->slaves); | ||
+ $conn->end(json_encode($result)); | ||
+ } | ||
+ | ||
+ protected function commandRegister(array $data, $conn) | ||
+ { | ||
+ $pid = (int)$data['pid']; | ||
+ $port = (int)$data['port']; | ||
+ $this->slaves[] = array( | ||
+ 'pid' => $pid, | ||
+ 'port' => $port, | ||
+ 'connection' => $conn | ||
+ ); | ||
+ if ($this->waitForSlaves && $this->slaveCount === count($this->slaves)) { | ||
+ $slaves = array(); | ||
+ foreach ($this->slaves as $slave) { | ||
+ $slaves[] = $slave['port']; | ||
+ } | ||
+ echo sprintf("%d slaves (%s) up and ready.\n", $this->slaveCount, implode(', ', $slaves)); | ||
+ } | ||
+ } | ||
+ | ||
+ protected function commandUnregister(array $data) | ||
+ { | ||
+ $pid = (int)$data['pid']; | ||
+ echo sprintf("Slave died. (pid %d)\n", $pid); | ||
+ foreach ($this->slaves as $idx => $slave) { | ||
+ if ($slave['pid'] === $pid) { | ||
+ unset($this->slaves[$idx]); | ||
+ $this->checkSlaves(); | ||
+ } | ||
+ } | ||
+ $this->checkSlaves(); | ||
+ } | ||
+ | ||
+ protected function checkSlaves() | ||
+ { | ||
+ if (!$this->run) { | ||
+ return; | ||
+ } | ||
+ | ||
+ $i = count($this->slaves); | ||
+ if ($this->slaveCount !== $i) { | ||
+ echo sprintf('Boot %d new slaves ... ', $this->slaveCount - $i); | ||
+ $this->waitForSlaves = true; | ||
+ for (; $i < $this->slaveCount; $i++) { | ||
+ $this->newInstance(); | ||
+ } | ||
+ } | ||
+ } | ||
+ | ||
+ function loop() | ||
+ { | ||
+ $this->loop->run(); | ||
+ } | ||
+ | ||
+ function newInstance() | ||
+ { | ||
+ $pid = pcntl_fork(); | ||
+ if (!$pid) { | ||
+ //we're in the slave now | ||
+ new ProcessSlave($this->getBridge()); | ||
+ exit; | ||
+ } | ||
+ } | ||
+} |
119
ProcessSlave.php
@@ -0,0 +1,119 @@ | ||
+<?php | ||
+ | ||
+namespace PHPPM; | ||
+ | ||
+class ProcessSlave | ||
+{ | ||
+ | ||
+ /** | ||
+ * @var \React\EventLoop\LibEventLoop|\React\EventLoop\StreamSelectLoop | ||
+ */ | ||
+ protected $loop; | ||
+ | ||
+ /** | ||
+ * @var resource | ||
+ */ | ||
+ protected $client; | ||
+ | ||
+ /** | ||
+ * @var \React\Socket\Connection | ||
+ */ | ||
+ protected $connection; | ||
+ | ||
+ /** | ||
+ * @var string | ||
+ */ | ||
+ protected $bridgeName; | ||
+ | ||
+ /** | ||
+ * @var Bridges\BridgeInterface | ||
+ */ | ||
+ protected $bridge; | ||
+ | ||
+ public function __construct($bridgeName = null) | ||
+ { | ||
+ $this->bridgeName = $bridgeName; | ||
+ $this->bootstrap(); | ||
+ $this->connectToMaster(); | ||
+ $this->loop->run(); | ||
+ } | ||
+ | ||
+ protected function shutdown() | ||
+ { | ||
+ echo "SHUTTING SLAVE PROCESS DOWN\n"; | ||
+ $this->bye(); | ||
+ exit; | ||
+ } | ||
+ | ||
+ /** | ||
+ * @return Bridges\BridgeInterface | ||
+ */ | ||
+ protected function getBridge() | ||
+ { | ||
+ if (null === $this->bridge && $this->bridgeName) { | ||
+ $bridgeClass = sprintf('PHPPM\Bridges\\%s', ucfirst($this->bridgeName)); | ||
+ $this->bridge = new $bridgeClass; | ||
+ } | ||
+ | ||
+ return $this->bridge; | ||
+ } | ||
+ | ||
+ protected function bootstrap() | ||
+ { | ||
+ if ($bridge = $this->getBridge()) { | ||
+ $bridge->bootstrap(); | ||
+ } | ||
+ } | ||
+ | ||
+ public function connectToMaster() | ||
+ { | ||
+ $this->loop = \React\EventLoop\Factory::create(); | ||
+ $this->client = stream_socket_client('tcp://127.0.0.1:5500'); | ||
+ $this->connection = new \React\Socket\Connection($this->client, $this->loop); | ||
+ | ||
+ $this->connection->on( | ||
+ 'close', | ||
+ \Closure::bind( | ||
+ function () { | ||
+ $this->shutdown(); | ||
+ }, | ||
+ $this | ||
+ ) | ||
+ ); | ||
+ | ||
+ $socket = new \React\Socket\Server($this->loop); | ||
+ $http = new \React\Http\Server($socket); | ||
+ $http->on('request', array($this, 'onRequest')); | ||
+ | ||
+ $port = 5501; | ||
+ while ($port < 5600) { | ||
+ try { | ||
+ $socket->listen($port); | ||
+ break; | ||
+ } catch( \React\Socket\ConnectionException $e ) { | ||
+ $port++; | ||
+ } | ||
+ } | ||
+ | ||
+ $this->connection->write(json_encode(array('cmd' => 'register', 'pid' => getmypid(), 'port' => $port))); | ||
+ } | ||
+ | ||
+ public function onRequest(\React\Http\Request $request, \React\Http\Response $response) | ||
+ { | ||
+ if ($bridge = $this->getBridge()) { | ||
+ return $bridge->onRequest($request, $response); | ||
+ } else { | ||
+ $response->writeHead('404'); | ||
+ $response->end('No Bridge Defined.'); | ||
+ } | ||
+ } | ||
+ | ||
+ public function bye() | ||
+ { | ||
+ if ($this->connection->isWritable()) { | ||
+ $this->connection->write(json_encode(array('cmd' => 'unregister', 'pid' => getmypid()))); | ||
+ $this->connection->close(); | ||
+ } | ||
+ $this->loop->stop(); | ||
+ } | ||
+} |
@@ -0,0 +1,12 @@ | ||
+PHP ProcessManager for ReactPHP | ||
+=============================== | ||
+ | ||
+This is the library used in my blog entry about high performance PHP applications. | ||
+ | ||
+First initial version. Final version will be pushed when the blog entry is published. | ||
+ | ||
+Example: | ||
+ | ||
+```bash | ||
+$ ./bin/ppm start ~/bude/symfony-24/ --bridge=symfony | ||
+``` |
15
bin/ppm
@@ -0,0 +1,15 @@ | ||
+#!/usr/bin/env php | ||
+<?php | ||
+ | ||
+set_time_limit(0); | ||
+ | ||
+$loader = require_once __DIR__ . '/../vendor/autoload.php'; | ||
+ | ||
+use Symfony\Component\Console\Application; | ||
+use PHPPM\Commands\StartCommand; | ||
+use PHPPM\Commands\StatusCommand; | ||
+ | ||
+$app = new Application('PHP-PM'); | ||
+$app->add(new StartCommand); | ||
+$app->add(new StatusCommand); | ||
+$app->run(); |
11
composer.json
@@ -0,0 +1,11 @@ | ||
+{ | ||
+ "require": { | ||
+ "symfony/console": "~2.4", | ||
+ "react/react": "0.3.3" | ||
+ }, | ||
+ "autoload": { | ||
+ "psr-4": { | ||
+ "PHPPM\\": "" | ||
+ } | ||
+ } | ||
+} |
0 comments on commit
1e63ad1