From 010f99b491f2f738dec68619eba0d232a6b896cf Mon Sep 17 00:00:00 2001 From: wedgwood Date: Sun, 23 Oct 2016 12:41:32 +0800 Subject: [PATCH] first commit --- .gitignore | 6 + build.php | 19 ++ composer.json | 18 ++ dist/apputl | 29 +++ dist/libserverbench.phar | Bin 0 -> 113012 bytes dist/serverbench.phar | Bin 0 -> 113020 bytes example/app-client/client.php | 54 +++++ example/benchmark/benchmark.php | 109 +++++++++ example/multier-client/client.php | 111 ++++++++++ example/use-as-lib/test.php | 10 + example/use-as-server/app.ini | 3 + example/use-as-server/app.php | 21 ++ example/use-as-server/apputil | 30 +++ example/with-codec/app.php | 5 + example/with-codec/apputil | 29 +++ example/with-codec/conf/app.ini | 3 + example/with-codec/log/.stub | 0 example/with-codec/src/App.php | 34 +++ logo.txt | 3 + readme.md | 80 +++++++ src/ServerBench/App/Client/Client.php | 174 +++++++++++++++ src/ServerBench/App/Client/Multier.php | 73 ++++++ src/ServerBench/App/Client/Poller.php | 79 +++++++ src/ServerBench/App/Server/Api.php | 75 +++++++ src/ServerBench/App/Server/PeriodicGc.php | 30 +++ src/ServerBench/App/Server/Server.php | 117 ++++++++++ src/ServerBench/Base/CliArguments.php | 52 +++++ src/ServerBench/Base/Errorable.php | 36 +++ src/ServerBench/Base/Gc.php | 31 +++ src/ServerBench/Base/Singleton.php | 34 +++ src/ServerBench/Codec/Decorator.php | 27 +++ src/ServerBench/Codec/Json.php | 21 ++ src/ServerBench/Codec/Php.php | 21 ++ src/ServerBench/Console/Colorizer.php | 68 ++++++ src/ServerBench/Controller/Controller.php | 101 +++++++++ src/ServerBench/Logger/ConsoleLogger.php | 34 +++ src/ServerBench/Logger/SysLogger.php | 29 +++ src/ServerBench/Process/Loop.php | 67 ++++++ src/ServerBench/Process/Meta.php | 13 ++ src/ServerBench/Process/Pool.php | 137 ++++++++++++ src/ServerBench/Process/Signal.php | 29 +++ src/ServerBench/Process/Util.php | 71 ++++++ src/ServerBench/Proxy/Proxy.php | 217 ++++++++++++++++++ src/ServerBench/Timer/PeriodicTask.php | 30 +++ src/ServerBench/Timer/Task.php | 36 +++ src/ServerBench/Timer/TaskQueue.php | 16 ++ src/ServerBench/Timer/Timer.php | 117 ++++++++++ src/ServerBench/Worker/Worker.php | 160 ++++++++++++++ src/ServerBench/cli/cli.php | 257 ++++++++++++++++++++++ src/ServerBench/cli/misc.php | 80 +++++++ src/ServerBench/cli/start_proxy.php | 75 +++++++ src/ServerBench/cli/start_worker.php | 78 +++++++ src/ServerBench/helpers.php | 28 +++ test/syslog.php | 25 +++ test/timer.php | 30 +++ version.txt | 1 + 56 files changed, 3033 insertions(+) create mode 100644 .gitignore create mode 100644 build.php create mode 100644 composer.json create mode 100644 dist/apputl create mode 100644 dist/libserverbench.phar create mode 100644 dist/serverbench.phar create mode 100644 example/app-client/client.php create mode 100644 example/benchmark/benchmark.php create mode 100644 example/multier-client/client.php create mode 100644 example/use-as-lib/test.php create mode 100644 example/use-as-server/app.ini create mode 100644 example/use-as-server/app.php create mode 100755 example/use-as-server/apputil create mode 100644 example/with-codec/app.php create mode 100755 example/with-codec/apputil create mode 100644 example/with-codec/conf/app.ini create mode 100644 example/with-codec/log/.stub create mode 100644 example/with-codec/src/App.php create mode 100644 logo.txt create mode 100644 readme.md create mode 100644 src/ServerBench/App/Client/Client.php create mode 100644 src/ServerBench/App/Client/Multier.php create mode 100644 src/ServerBench/App/Client/Poller.php create mode 100644 src/ServerBench/App/Server/Api.php create mode 100644 src/ServerBench/App/Server/PeriodicGc.php create mode 100644 src/ServerBench/App/Server/Server.php create mode 100644 src/ServerBench/Base/CliArguments.php create mode 100644 src/ServerBench/Base/Errorable.php create mode 100644 src/ServerBench/Base/Gc.php create mode 100644 src/ServerBench/Base/Singleton.php create mode 100644 src/ServerBench/Codec/Decorator.php create mode 100644 src/ServerBench/Codec/Json.php create mode 100644 src/ServerBench/Codec/Php.php create mode 100644 src/ServerBench/Console/Colorizer.php create mode 100644 src/ServerBench/Controller/Controller.php create mode 100644 src/ServerBench/Logger/ConsoleLogger.php create mode 100644 src/ServerBench/Logger/SysLogger.php create mode 100644 src/ServerBench/Process/Loop.php create mode 100644 src/ServerBench/Process/Meta.php create mode 100644 src/ServerBench/Process/Pool.php create mode 100644 src/ServerBench/Process/Signal.php create mode 100644 src/ServerBench/Process/Util.php create mode 100644 src/ServerBench/Proxy/Proxy.php create mode 100644 src/ServerBench/Timer/PeriodicTask.php create mode 100644 src/ServerBench/Timer/Task.php create mode 100644 src/ServerBench/Timer/TaskQueue.php create mode 100644 src/ServerBench/Timer/Timer.php create mode 100644 src/ServerBench/Worker/Worker.php create mode 100644 src/ServerBench/cli/cli.php create mode 100644 src/ServerBench/cli/misc.php create mode 100644 src/ServerBench/cli/start_proxy.php create mode 100644 src/ServerBench/cli/start_worker.php create mode 100644 src/ServerBench/helpers.php create mode 100644 test/syslog.php create mode 100644 test/timer.php create mode 100644 version.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5c494a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +tags +vendor +#composer.phar +composer.lock +*.log +.pid diff --git a/build.php b/build.php new file mode 100644 index 0000000..a7c590a --- /dev/null +++ b/build.php @@ -0,0 +1,19 @@ +startBuffering(); +$phar->buildFromDirectory(__DIR__, '$(src|vendor)/.*|(logo|version)\.txt$'); +// $phar->compressFiles(Phar::GZ); +$phar->setStub($phar->createDefaultStub('./vendor/autoload.php')); +$phar->stopBuffering(); + +$phar = new Phar($bin); +$phar->startBuffering(); +$phar->buildFromDirectory(__DIR__, '$(src|vendor)/.*|(logo|version)\.txt$'); +// $phar->compressFiles(Phar::GZ); +$phar->setStub($phar->createDefaultStub('./src/ServerBench/cli/cli.php')); +$phar->stopBuffering(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..31d16e1 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "serverbench/serverbench", + "description": "a serverbench implemented using php", + "keywords": ["ipc", "rpc", "net", "concurrent"], + "require": { + "php": ">=5.4.0", + "ext-posix": "*", + "ext-pcntl": "*", + "ext-Phar": "*", + "psr/log": "1.0.0" + }, + "autoload": { + "psr-4": {"ServerBench\\": "src/ServerBench"}, + "files": [ + "src/ServerBench/helpers.php" + ] + } +} diff --git a/dist/apputl b/dist/apputl new file mode 100644 index 0000000..8605735 --- /dev/null +++ b/dist/apputl @@ -0,0 +1,29 @@ +#!/bin/bash +if [[ $# -lt 1 ]]; then + echo 'usage: apputil {start|stop|ps}' +fi + +app_dir="$(cd "$(dirname "$0")" && pwd -P)" +cd $app_dir + +if [[ $1 = "start" ]] +then + php ./serverbench.phar --pidfile=$app_dir/pid --dir=$app_dir --app=$app_dir/app.php "${@:2}" +elif [[ $1 = "stop" ]] +then + php ./dist/serverbench.phar --stop --pidfile=$app_dir/pid --dir=$app_dir +elif [[ $1 = "ps" ]] + php ../../dist/serverbench.phar --status --pidfile=$app_dir/pid --dir=$app_dir +then + if [ -f './pid' ] + then + echo -e "\e[0;32m" + pid=`cat pid` + ps -o pid,ppid,pgid,sess,tt,user,start_time,time,stat,%cpu,rss,vsz,size,%mem,cmd --pid $pid --ppid $pid + echo -e "\e[0m" + else + echo -e "\e[0;31m" + echo "no any running process found!" + echo -e "\e[0m" + fi +fi diff --git a/dist/libserverbench.phar b/dist/libserverbench.phar new file mode 100644 index 0000000000000000000000000000000000000000..c7cbea02a18491567bf6ac2858c76c0f35ad3ab1 GIT binary patch literal 113012 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 H|GobYy+7YF literal 0 HcmV?d00001 diff --git a/example/app-client/client.php b/example/app-client/client.php new file mode 100644 index 0000000..f6bbce6 --- /dev/null +++ b/example/app-client/client.php @@ -0,0 +1,54 @@ +connect('tcp://127.0.0.1:12345', new ServerBench\Codec\Json(), false); + +$start = mt(); +$msg = <<send(['data' => $msg, 'seq' => $i]); + + if (false === $rc) { + echo 'failed to send msg. ', $app->errno(), "\n"; + die(); + } + + $reply = $app->recv(); + + if (false === $reply) { + echo 'failed to recv msg. ', $app->errno(), "\n"; + die(); + } + + assert($reply['seq'] == $i); +} + +$end = mt(); +$delta = $end - $start; +$speed = 1000 / $delta * 1000; + +echo "use {$delta} ms, speed {$speed} pkgs/s \n"; diff --git a/example/benchmark/benchmark.php b/example/benchmark/benchmark.php new file mode 100644 index 0000000..6f930d5 --- /dev/null +++ b/example/benchmark/benchmark.php @@ -0,0 +1,109 @@ + 'concurrent:', + 'C:' => 'connect:', + 'T:' => 'time:', + 'L:' => 'length:' +]); + +function mt() +{ + return gettimeofday(true) * 1000; +} + +if (!isset($arguments['concurrent']) && + !isset($arguments['connect']) && + !isset($arguments['time']) +) { + die("usage: benchmark.php -c {num of clients} -C {address} -T {timed testing}"); +} + +define('NUM_OF_CLIENTS', $arguments->get('concurrent')); +define('CONNECT', $arguments->get('connect')); +define('TIME_TO_TESTING', $arguments->get('time') * 1000); +define('LENGTH', $arguments->get('length', 100)); + +printf( + "clients %d, connect %s, time to testing %f sec, length %d\n", + NUM_OF_CLIENTS, + CONNECT, + TIME_TO_TESTING / 1000, + LENGTH +); + +// ready +$recv = []; +$clients = []; +$poller = new Poller(); + +$msg = str_repeat('-', LENGTH); + +for ($i = 0; $i < NUM_OF_CLIENTS; ++$i) { + $client = new ClientApp(); + $client->setSndHwm(1000000); + $client->setRcvHwm(1000000); + $client->connect(CONNECT, null, true); + $client->setRecvTimeout(200); + $client->setSendTimeout(200); + $id = $poller->registerReadable($client); + $clients[$id] = $client; + $recv[$id] = 0; +} + +$sending = 0; +$recving = 0; + +$start = mt(); + +foreach ($clients as $id => $client) { + $client->send($msg); +} + +while (mt() - $start < TIME_TO_TESTING) { + $rset = []; + $wset = []; + + $events = $poller->poll($rset, $wset, 0); + + if ($events > 0) { + foreach ($rset as $id) { + $client = $clients[$id]; + $msg = $client->recv(); + + if (isset($msg) && $client->errno()) { + echo 'failed to recv msg. ', $client->errstr(), "\n"; + die(); + } + + ++$recv[$id]; + $client->send($msg); + } + } +} + +$end = mt(); + +$delta = $end - $start; +$total = array_sum($recv); +$max = max($recv); +$min = min($recv); + +$speed_total = $total / $delta * 1000; +$speed_avg = $speed_total / NUM_OF_CLIENTS; +$speed_high = $max / $delta * 1000; +$speed_low = $min / $delta * 1000; + +echo "speed total {$speed_total} pkgs/sec\n"; +echo "speed avg {$speed_avg} pkgs/sec\n"; +echo "speed high {$speed_high} pkgs/sec\n"; +echo "speed low {$speed_low} pkgs/sec\n"; diff --git a/example/multier-client/client.php b/example/multier-client/client.php new file mode 100644 index 0000000..986c8ee --- /dev/null +++ b/example/multier-client/client.php @@ -0,0 +1,111 @@ +setSndHwm(1000000); + $client->setRcvHwm(1000000); + $client->connect('tcp://127.0.0.1:12345', new ServerBench\Codec\Json(), true); + $client->setRecvTimeout(200); + $client->setSendTimeout(200); + $clients[] = $client; +} + +$msg = <<fetch($clients, $block ? -1 : 0); + + if (false === $reply) { + echo 'multier failed to recv msg. ', $m->errstr(), "\n"; + die(); + } + + if (empty($reply)) { + return; + } + + for ($i = 0; $i < NUM_OF_CLIENTS; ++$i) { + if (!isset($reply[$i]) && $clients[$i]->errno()) { + echo 'failed to recv msg. ', $clients[$i]->errstr(), "\n"; + die(); + } + + if (isset($reply[$i])) { + unset($map[$reply[$i]['seq']]); + ++$recving; + } + } +} + +while ($sending < NUM_OF_PACKAGES) { + $j = $sending % NUM_OF_CLIENTS; + $rc = $clients[$j]->send(['data' => $msg, 'seq' => $sending]); + + if (false === $rc) { + printf( + "failed to send msg. %d:%s\n", + $clients[$j]->errno(), + $clients[$j]->errstr() + ); + die(); + } else { + $map[$sending] = 1; + ++$sending; + } + + if ($sending % 10 == 0) { + doRecv(); + } +} + +while ($sending > $recving) { + printf("\rrecving(%d) < $sending(%d)", $recving, $sending); + doRecv(true); +} + +printf("\rrecving(%d) == $sending(%d)\n", $recving, $sending); + +$end = mt(); +$delta = $end - $start; +$speed = NUM_OF_PACKAGES / $delta * 1000; +$mps = $speed * 100 / 1024 / 1024; + +echo "use {$delta} ms, speed {$speed} pkgs/sec, {$mps} mb/sec\n"; diff --git a/example/use-as-lib/test.php b/example/use-as-lib/test.php new file mode 100644 index 0000000..464a234 --- /dev/null +++ b/example/use-as-lib/test.php @@ -0,0 +1,10 @@ +setProcessNum(10); +$server->run(); diff --git a/example/use-as-server/app.ini b/example/use-as-server/app.ini new file mode 100644 index 0000000..c12e0d8 --- /dev/null +++ b/example/use-as-server/app.ini @@ -0,0 +1,3 @@ +daemonize=0 +listen=tcp://127.0.0.1:12345 +workers=8 diff --git a/example/use-as-server/app.php b/example/use-as-server/app.php new file mode 100644 index 0000000..2118534 --- /dev/null +++ b/example/use-as-server/app.php @@ -0,0 +1,21 @@ +real_process_ = new CodecDecorator(new JsonCodec(), realProcess); + } + + public function init() + { + return true; + } + + public function fini() + { + return true; + } + + public function process($msg) + { + return call_user_func($this->real_process_, $msg); + } +} diff --git a/logo.txt b/logo.txt new file mode 100644 index 0000000..5ffc886 --- /dev/null +++ b/logo.txt @@ -0,0 +1,3 @@ + ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐┌┐ ┌─┐┌┐┌┌─┐┬ ┬ +/* └─┐├┤ ├┬┘└┐┌┘├┤ ├┬┘├┴┐├┤ ││││ ├─┤ */ + └─┘└─┘┴└─ └┘ └─┘┴└─└─┘└─┘┘└┘└─┘┴ ┴ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..29718d0 --- /dev/null +++ b/readme.md @@ -0,0 +1,80 @@ +# serverbench.php +--- + +### Requirements + +- posix +- php-pcntl (or php-cli) +- [proctitle](http://pecl.php.net/package/proctitle) (optional) +- [libzmq](https://github.com/zeromq/libzmq) +- [php-zmq](https://github.com/mkoppanen/php-zmq) + +### example + +- [use as lib](../tree/develop/example/use-as-lib/) + +```php +run($daemon); +``` + +- [use as server](../tree/develop/example/use-as-server/) + +```bash +#cli utils + +#start +php serverbench.phar --pidfile=./pid --dir=./ --app=app.php -c app.ini --daemon + +#stop +php serverbench.phar --stop --pidfile=./pid + +#reload +php serverbench.phar --reload --pidfile=./pid + +#status +php serverbench.phar --status --pidfile=./pid +``` + +```php +sndhwm_ = $hwm; + } + + public function setRcvHwm($hwm) + { + $this->rcvhwm_ = $hwm; + } + + public function connect($addr, $codec = null, $nonblocking = false) + { + $this->clearErr_(); + $ret = false; + + do { + try { + $zctx = new ZMQContext(); + $this->socket_ = $zctx->getSocket(ZMQ::SOCKET_DEALER); + $this->socket_->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + $this->socket_->setSockOpt(ZMQ::SOCKOPT_SNDHWM, $this->sndhwm_); + $this->socket_->setSockOpt(ZMQ::SOCKOPT_RCVHWM, $this->rcvhwm_); + $this->socket_->connect($addr); + } catch (\Exception $e) { + $this->setErr_(-1, (string)$e); + break; + } + + $this->codec_ = $codec; + + if ($nonblocking) { + $this->enableNonblocking(); + } else { + $this->disableNonblocking(); + } + + $ret = true; + } while (0); + + return $ret; + } + + public function enableNonblocking() + { + $this->mode_ = ZMQ::MODE_NOBLOCK; + } + + public function disableNonblocking() + { + $this->mode_ = 0; + } + + public function setSendTimeout($timeout) + { + $this->clearErr_(); + $ret = false; + + try { + $this->socket_->setSockOpt(ZMQ::SOCKOPT_SNDTIMEO, $timeout); + $ret = true; + } catch (ZMQSocketException $e) { + $this->setErr_(-1, (string)$e); + } + + return $ret; + } + + public function setRecvTimeout($timeout) + { + $this->clearErr_(); + $ret = false; + + try { + $this->socket_->setSockOpt(ZMQ::SOCKOPT_RCVTIMEO, $timeout); + $ret = true; + } catch (ZMQSocketException $e) { + $this->setErr_(-1, (string)$e); + } + + return $ret; + } + + public function send($data) + { + $this->clearErr_(); + $ret = false; + + try { + if ($this->codec_) { + $this->socket_->sendmulti(['', $this->codec_->encode($data)], $this->mode_); + } else { + $this->socket_->sendmulti(['', $data], $this->mode_); + } + + $ret = true; + } catch (\Exception $e) { + $this->setErr_(-1, (string)$e); + } + + return $ret; + } + + public function recv() + { + $this->clearErr_(); + $ret = null; + + do { + try { + $msg = $this->socket_->recvMulti($this->mode_); + + if ($msg !== false) { + if (count($msg) !== 2 || !empty($msg[0])) { + $this->setErr_(-1, 'invalid message received.'); + break; + } + } + + if ($this->codec_) { + $ret = $this->codec_->decode($msg[1]); + } else { + $ret = $msg[1]; + } + } catch (\Exception $e) { + $this->setErr_(-1, (string)$e); + } + } while (0); + + return $ret; + } + + public function request($data) + { + $ret = false; + + if ($this->send($data)) { + $ret = $this->recv(); + } + + return $ret; + } + + public function getSocket() + { + return $this->socket_; + } +} diff --git a/src/ServerBench/App/Client/Multier.php b/src/ServerBench/App/Client/Multier.php new file mode 100644 index 0000000..ee93aff --- /dev/null +++ b/src/ServerBench/App/Client/Multier.php @@ -0,0 +1,73 @@ + $client) { + $id = $poller->registerReadable($client); + $poll_ids[$id] = $i; + } + + $ms_left = $ms; + $ret = []; + + do { + $rset = []; + $wset = []; + $events = 0; + + if ($ms_left > 0) { + $tv1_ms = gettimeofday(true) * 1000; + $events = $poller->poll($rset, $wset, $ms_left); + $tv2_ms = gettimeofday(true) * 1000; + $ms_left -= ($tv2_ms - $tv1_ms); + + if ($ms_left < 0) { + $ms_left = 0; + } + } else { + $events = $poller->poll($rset, $wset, $ms_left); + } + + // $errors = $poller->getLastErrors(); + + // if (count($errors) > 0) { + // $errno = -1; + // $errstr = []; + + // foreach ($errors as $error) { + // $errstr[] = $error; + // } + + // $this->setErr_($errno, implode(',', $errstr)); + // $ret = false; + // } + + if ($events > 0) { + foreach ($rset as $id) { + $i = $poll_ids[$id]; + $msg = $clients[$i]->recv(); + + if ($msg !== false && !$client->errno()) { + $ret[$i] = $msg; + } + + $poller->unregister($client); + } + } + } while ($poller->count() && $ms_left > 0); + + return $ret; + } +} diff --git a/src/ServerBench/App/Client/Poller.php b/src/ServerBench/App/Client/Poller.php new file mode 100644 index 0000000..f4bf405 --- /dev/null +++ b/src/ServerBench/App/Client/Poller.php @@ -0,0 +1,79 @@ +poller_ = new ZMQPoll(); + } + + public function registerReadable($client) + { + $socket = $client->getSocket(); + $hash = spl_object_hash($socket); + $this->poller_->add($socket, ZMQ::POLL_IN); + return $hash; + } + + public function unregister($client) + { + $this->poller_->remove($client->getSocket()); + } + + public function registerWritable($client) + { + $socket = $client->getSocket(); + $hash = spl_object_hash($socket); + $this->poller_->add($socket, ZMQ::POLL_OUT); + return $hash; + } + + public function registerAllEvents($client) + { + $socket = $client->getSocket(); + $hash = spl_object_hash($socket); + $this->poller_->add($socket, ZMQ::POLL_IN | ZMQ::POLL_OUT); + return $hash; + } + + public function poll(&$rset = [], &$wset = [], $ms = -1) + { + $readable = []; + $writable = []; + $events = $this->poller_->poll($readable, $writable, $ms); + + if ($events > 0) { + foreach ($readable as $socket) { + $rset[] = spl_object_hash($socket); + } + + foreach ($writable as $socket) { + $wset[] = spl_object_hash($socket); + } + } + + return $events; + } + + public function count() + { + return $this->poller_->count(); + } +} diff --git a/src/ServerBench/App/Server/Api.php b/src/ServerBench/App/Server/Api.php new file mode 100644 index 0000000..6ee263b --- /dev/null +++ b/src/ServerBench/App/Server/Api.php @@ -0,0 +1,75 @@ +as_ = $as; + + if (is_object($app) && method_exists($app, 'process')) { + if (method_exists($app, 'init')) { + $this->init_ = [$app, 'init']; + } + + if (method_exists($app, 'fini')) { + $this->init_ = [$app, 'fini']; + } + + if (method_exists($app, 'process')) { + if (isset($codec)) { + $this->process_ = new CodecDecorator($codec, [$app, 'process']); + } else { + $this->process_ = [$app, 'process']; + } + } else { + throw InvalidArgumentException('app\'s process method must be set.'); + } + } elseif (is_callable($app)) { + $this->process_ = $app; + } else { + throw InvalidArgumentException('no callable or object for app\'s implementation found.'); + } + } + + public function init() + { + $ret = true; + + if (isset($this->init_)) { + $ret = call_user_func($this->init_, $this->as_); + } + + return $ret; + } + + public function fini() + { + $ret = true; + + if (isset($this->fini_)) { + $ret = call_user_func($this->fini_, $this->as_); + } + + return $ret; + } + + public function process($message) + { + return call_user_func($this->process_, $message); + } +} diff --git a/src/ServerBench/App/Server/PeriodicGc.php b/src/ServerBench/App/Server/PeriodicGc.php new file mode 100644 index 0000000..02cdb56 --- /dev/null +++ b/src/ServerBench/App/Server/PeriodicGc.php @@ -0,0 +1,30 @@ +runEvery($sec, function () { + Gc::trigger(); + }); + } + + public static function disable() + { + self::$task_->cancel(); + Gc::disable(); + } +} diff --git a/src/ServerBench/App/Server/Server.php b/src/ServerBench/App/Server/Server.php new file mode 100644 index 0000000..011416d --- /dev/null +++ b/src/ServerBench/App/Server/Server.php @@ -0,0 +1,117 @@ +listen_addr_ = $listen_addr; + $this->message_callback_ = $message_callback; + $this->title_ = 'serverbench'; + $this->sock_dir_ = $this->dir_ = getcwd(); + } + + public function setMessageCallback($callback) + { + $this->message_callback_ = $callback; + } + + public function setInitCallback($callback) + { + $this->init_callback_ = $callback; + } + + public function setTitle($title) + { + $this->title_ = $title; + } + + public function setProcessNum($num) + { + $this->process_num_ = $num; + } + + public function setDir($dir) + { + $this->dir_ = $dir; + } + + public function setSockDir($dir) + { + $this->sock_dir_ = $dir; + } + + public function run($daemon = false) + { + if ($daemon) { + ProcessUtil::daemon(); + } + + $rand = mt_rand(); + $title = $this->title_; + + $ipcs = array( + sprintf('ipc:///%s/ipc%d_%s_0.sock', $this->sock_dir_, $rand, $this->title_), + sprintf('ipc:///%s/ipc%d_%s_1.sock', $this->sock_dir_, $rand, $this->title_) + ); + + ProcessUtil::setTitle(sprintf('%s', $title)); + + PeriodicGc::enable(300); + + $controller = new Controller(); + + $controller->run( + array( + 'groups' => array( + array( + 'proxy' => array( + 'routine' => function () use ($ipcs, $title) { + ProcessUtil::setTitle(sprintf('%s', $title)); + $proxy = new Proxy($this->listen_addr_, $ipcs); + $proxy->run(); + } + ), + 'worker' => array( + 'routine' => function () use ($ipcs, $title) { + ProcessUtil::setTitle(sprintf('%s', $title)); + + if (isset($this->init_callback_)) { + if (false === call_user_func($this->init_callback_)) { + \ServerBench\syslog_error('init_callback returns false.'); + return; + } + } + + $worker = new Worker($ipcs, $this->message_callback_); + $worker->run(); + }, + 'num' => $this->process_num_ < 1 ? 1 : $this->process_num_ + ) + ) + ) + ), + $this->dir_ + ); + } +} diff --git a/src/ServerBench/Base/CliArguments.php b/src/ServerBench/Base/CliArguments.php new file mode 100644 index 0000000..ce6f635 --- /dev/null +++ b/src/ServerBench/Base/CliArguments.php @@ -0,0 +1,52 @@ + $v) { + if (is_numeric($k)) { + if (strlen($v) == 1) { + $opts[] = $v; + } else { + $longopts[] = $v; + } + } else { + $opts[] = $k; + $longopts[] = $v; + $k = trim($k, ':'); + $v = trim($v, ':'); + $relations[$k] = $v; + $relations[$v] = $k; + } + } + + $arguments = array(); + + foreach (getopt(implode('', $opts), $longopts) as $k => $v) { + if (isset($relations[$k]) && !isset($arguments[$relations[$k]])) { + $arguments[$relations[$k]] = $v; + } + + $arguments[$k] = $v; + } + + parent::__construct($arguments, \ArrayObject::STD_PROP_LIST | \ArrayObject::ARRAY_AS_PROPS); + } + + public function get($option, $default = null) + { + return isset($this[$option]) ? $this[$option] : $default; + } +} diff --git a/src/ServerBench/Base/Errorable.php b/src/ServerBench/Base/Errorable.php new file mode 100644 index 0000000..06bc85a --- /dev/null +++ b/src/ServerBench/Base/Errorable.php @@ -0,0 +1,36 @@ +errno_; + } + + public function errstr() + { + return $this->errstr_; + } + + protected function clearErr_() + { + $this->errstr_ = ''; + $this->errno_ = 0; + } + + protected function setErr_($no, $str) + { + $this->errstr_ = $str; + $this->errno_ = $no; + } +} diff --git a/src/ServerBench/Base/Gc.php b/src/ServerBench/Base/Gc.php new file mode 100644 index 0000000..2f27b73 --- /dev/null +++ b/src/ServerBench/Base/Gc.php @@ -0,0 +1,31 @@ +codec_ = $codec; + $this->fn_ = $fn; + } + + public function __invoke($message) + { + $req = $this->codec_->decode($message); + $rep = call_user_func($this->fn_, $req); + return $this->codec_->encode($rep); + } +} diff --git a/src/ServerBench/Codec/Json.php b/src/ServerBench/Codec/Json.php new file mode 100644 index 0000000..9c4d239 --- /dev/null +++ b/src/ServerBench/Codec/Json.php @@ -0,0 +1,21 @@ + "\033[0m", + 'black' => "\033[30m", + 'red' => "\033[31m", + 'green' => "\033[32m", + 'yellow' => "\033[33m", + 'blue' => "\033[34m", + 'purple' => "\033[35m", + 'cyan' => "\033[36m", + 'white' => "\033[37m" + ); + + public static function color($name, $msg) + { + return sprintf("%s%s\033[0m", self::$colors_[$name], $msg); + } + + public static function black($msg) + { + return self::color('black', $msg); + } + + public static function red($msg) + { + return self::color('red', $msg); + } + + public static function green($msg) + { + return self::color('green', $msg); + } + + public static function yellow($msg) + { + return self::color('yellow', $msg); + } + + public static function blue($msg) + { + return self::color('blue', $msg); + } + + public static function purple($msg) + { + return self::color('purple', $msg); + } + + public static function cyan($msg) + { + return self::color('cyan', $msg); + } + + public static function white($msg) + { + return self::color('white', $msg); + } +} diff --git a/src/ServerBench/Controller/Controller.php b/src/ServerBench/Controller/Controller.php new file mode 100644 index 0000000..3efdd7c --- /dev/null +++ b/src/ServerBench/Controller/Controller.php @@ -0,0 +1,101 @@ +start(1, $proxy_conf['routine']); + $group['proxy'] = $proxy_pool; + } + + if (isset($group_conf['worker'])) { + $worker_conf = $group_conf['worker']; + $worker_pool = new Pool(); + $worker_pool->start($worker_conf['num'], $worker_conf['routine']); + $group['worker'] = $worker_pool; + } + + $groups[] = $group; + } + + sleep(1); + $bootstrap_success = true; + + foreach ($groups as $group) { + foreach ($group as $pool) { + $pool->waitAll(); + + if ($pool->getWorkersDied() > 0) { + $bootstrap_success = false; + break; + } + } + } + + if ($bootstrap_success) { + $loop = Loop::getInstance(); + $loop->start(); + + $restarting_workers = false; + + Signal::getInstance()->on(SIGUSR1, function () use (&$restarting_workers) { + $restarting_workers = true; + }); + + while ($loop->running()) { + if ($restarting_workers) { + foreach ($groups as $group) { + if (isset($group['worker'])) { + $group['worker']->killAll(SIGTERM); + } + } + + $restarting_workers = false; + } + + foreach ($groups as $group) { + foreach ($group as $pool) { + $pool->keep(); + } + } + + usleep(40000); + } + } else { + \ServerBench\syslog_error('failed to bootstrap workers in controller.'); + } + + foreach ($groups as $group) { + foreach ($group as $pool) { + $pool->terminate(); + } + } + } +} diff --git a/src/ServerBench/Logger/ConsoleLogger.php b/src/ServerBench/Logger/ConsoleLogger.php new file mode 100644 index 0000000..7e8aa88 --- /dev/null +++ b/src/ServerBench/Logger/ConsoleLogger.php @@ -0,0 +1,34 @@ + 'red', + 'alert' => 'red', + 'critical' => 'red', + 'error' => 'red', + 'warning' => 'yellow', + 'notice' => 'yellow', + 'info' => 'green', + 'debug' => 'blue' + ]; + + public function log($level, $message, array $context = []) + { + echo Colorizer::color($this->colors_[$level], sprintf("%-9s", $level)), + "\t", vsprintf($message, $context), "\n"; + } +} diff --git a/src/ServerBench/Logger/SysLogger.php b/src/ServerBench/Logger/SysLogger.php new file mode 100644 index 0000000..5a55bb9 --- /dev/null +++ b/src/ServerBench/Logger/SysLogger.php @@ -0,0 +1,29 @@ +setLogger(ConsoleLogger::getInstance()); + } + + public function log($level, $message, array $context = array()) + { + $this->logger->log($level, vsprintf($message, $context)); + } +} diff --git a/src/ServerBench/Process/Loop.php b/src/ServerBench/Process/Loop.php new file mode 100644 index 0000000..eda9718 --- /dev/null +++ b/src/ServerBench/Process/Loop.php @@ -0,0 +1,67 @@ +reset(); + + $signal = Signal::getInstance(); + $signal->on(SIGHUP, SIG_IGN); + $signal->on(SIGPIPE, SIG_IGN); + $signal->on(SIGINT, [$this, 'stop']); + $signal->on(SIGQUIT, [$this, 'stop']); + $signal->on(SIGTERM, [$this, 'stop']); + + $this->start(); + } + + public function running() + { + Signal::getInstance()->dispatch(); + return $this->running_; + } + + public function stop() + { + $this->running_ = false; + } + + public function start() + { + $this->started_ = true; + $this->running_ = true; + return $this; + } + + public function reset() + { + $this->started_ = false; + $this->running_ = false; + return $this; + } + + public function __invoke() + { + if (!$this->started_) { + $this->start(); + } + + return $this->running(); + } +} diff --git a/src/ServerBench/Process/Meta.php b/src/ServerBench/Process/Meta.php new file mode 100644 index 0000000..de4ed94 --- /dev/null +++ b/src/ServerBench/Process/Meta.php @@ -0,0 +1,13 @@ + 0) { + $meta = new Meta(); + $meta->begin_ts = time(); + $this->workers_[$pid] = $meta; + } + + return $pid; + } + + public function spawn($path, $args = [], $envs = []) + { + $ret = false; + $pid = $this->fork(); + + if (0 === $pid) { + if (false === pcntl_exec($path, $args, $envs)) { + exit(0); + } + } elseif ($pid > 0) { + $ret = $pid; + } else { + // nothing to do ... + } + + return $ret; + } + + private function recycle_($pid) + { + unset($this->workers_[$pid]); + } + + public function waitAll($block = false) + { + foreach ($this->workers_ as $pid => $worker) { + $pid = pcntl_waitpid($pid, $status, $block ? 0 : WNOHANG); + + if ($pid > 0) { + $this->recycle_($pid); + } + } + } + + public function killAll($sig) + { + foreach ($this->workers_ as $pid => $meta) { + posix_kill($pid, $sig); + } + } + + public function cleanUp() + { + foreach ($this->workers_ as $pid => $meta) { + if (!posix_kill($pid, 0)) { + unset($this->workers_[$pid]); + } + } + } + + public function terminate($block = true) + { + $this->cleanUp(); + $this->killAll(SIGTERM); + $this->waitAll($block); + } + + public function keep() + { + $this->waitAll(false); + $this->cleanUp(); + + $need = $this->num_ - count($this->workers_); + + for ($i = 0; $i < $need; ++$i) { + call_user_func($this->routine_); + } + } + + public function start($num, $routine) + { + if (is_callable($routine)) { + $this->routine_ = function () use ($routine) { + if (0 === $this->fork()) { + $routine(); + exit(0); + } + }; + } elseif (is_string($routine)) { + $this->routine_ = function () use ($routine) { + $this->spawn($routine); + }; + } elseif (is_array($routine)) { + $this->routine_ = function () use ($routine) { + call_user_func_array([$this, 'spawn'], $routine); + }; + } else { + return false; + } + + $this->num_ = $num; + $this->keep(); + } + + public function getWorkersNum($real = true) + { + if ($real) { + return count($this->workers_); + } + + return $this->num_; + } + + public function getWorkersDied() + { + return $this->num_ - count($this->workers_); + } +} diff --git a/src/ServerBench/Process/Signal.php b/src/ServerBench/Process/Signal.php new file mode 100644 index 0000000..a7c66ea --- /dev/null +++ b/src/ServerBench/Process/Signal.php @@ -0,0 +1,29 @@ + 0) { + exit(); + } elseif ($pid < 0) { + return false; + } else { + // nothing to do ... + } + + $pid = pcntl_fork(); + + if ($pid > 0) { + exit(); + } elseif ($pid < 0) { + return false; + } else { + // nothing to do ... + } + + $sid = posix_setsid(); + + if ($sid < 0) { + return false; + } + + if (!$nochdir) { + chdir('/'); + } + + umask(0); + + if (!$noclose) { + fclose(STDIN); + fclose(STDOUT); + fclose(STDERR); + } + + return true; + } +} diff --git a/src/ServerBench/Proxy/Proxy.php b/src/ServerBench/Proxy/Proxy.php new file mode 100644 index 0000000..2e88ce2 --- /dev/null +++ b/src/ServerBench/Proxy/Proxy.php @@ -0,0 +1,217 @@ +max_wait_ms_ = $ms; + } + + public function __construct($listen_addr, $ipcs) + { + $zctx = new ZMQContext(1, false); + $this->zctx_ = $zctx; + $acceptor = new ZMQSocket($zctx, ZMQ::SOCKET_ROUTER); + + $acceptor->setSockOpt(ZMQ::SOCKOPT_RCVHWM, 1024 * 1024 * 16); + $acceptor->setSockOpt(ZMQ::SOCKOPT_SNDHWM, 1024 * 1024 * 32); + $acceptor->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + + foreach ((array)$listen_addr as $item) { + $acceptor->bind($item); + } + + $this->acceptor_ = $acceptor; + + $ipc0 = new ZMQSocket($zctx, ZMQ::SOCKET_PUSH); + $ipc0->setSockOpt(ZMQ::SOCKOPT_HWM, 1024 * 1024 * 16); + $ipc0->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + $ipc0->bind($ipcs[0]); + $this->ipc0_ = $ipc0; + + $ipc1 = new ZMQSocket($zctx, ZMQ::SOCKET_PULL); + $ipc1->setSockOpt(ZMQ::SOCKOPT_HWM, 1024 * 1024 * 16); + $ipc1->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + $ipc1->bind($ipcs[1]); + $this->ipc1_ = $ipc1; + } + + public function run() + { + $acceptor = $this->acceptor_; + $ipc0 = $this->ipc0_; + $ipc1 = $this->ipc1_; + + $timer = Timer::getInstance(); + + $poller = new ZMQPoll(); + $poller->add($acceptor, ZMQ::POLL_IN); + $poller->add($ipc1, ZMQ::POLL_IN); + + $loop = Loop::getInstance(); + $loop->start(); + + while ($loop->running()) { + // not execute timer too many times + $timer->execute(64); + + $writable = []; + $readable = []; + + try { + if ($timer->isEmpty()) { + $events = $poller->poll($readable, $writable, $this->max_wait_ms_); + } else { + $nearest_delta_ms = $timer->nearestDeltaTimeMs(); + + if ($this->max_wait_ms_ > 0 && $this->max_wait_ms_ < $nearest_delta_ms) { + $real_wait_ms = $this->max_wait_ms_; + } else { + if ($nearest_delta_ms < 0) { + $nearest_delta_ms = 0; + } + + $real_wait_ms = $nearest_delta_ms; + } + + $events = $poller->poll($readable, $writable, $real_wait_ms); + } + } catch (ZMQPollException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + + if ($events > 0) { + foreach ($readable as $socket) { + if ($socket === $ipc1) { + while (true) { + try { + $msg = $socket->recvMulti(ZMQ::MODE_NOBLOCK); + + if (false === $msg) { + // would block + break; + } + + if (count($msg) !== 3 || !empty($msg[1])) { + \ServerBench\syslog_error('invalid msg(%s) from worker.', [var_export($msg, true)]); + break; + } + + $rc = false; + + do { + try { + $rc = $acceptor->sendmulti([$msg[0], '', $msg[2]], ZMQ::MODE_NOBLOCK); + } catch (ZMQSocketException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + + break; + } while (true); + + if (false === $rc) { + \ServerBench\syslog_error( + 'sending\'s mq is out of memory, msg(%s) would lose', + [$msg[2]] + ); + } + } catch (ZMQSocketException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + } + } elseif ($socket === $acceptor) { + // acceptor + for ($i = 0; $i < 4096; ++$i) { + try { + $msg = $acceptor->recvMulti(ZMQ::MODE_NOBLOCK); + + if (false === $msg) { + break; + } + + if (count($msg) !== 3 || !empty($msg[1])) { + \ServerBench\syslog_error('invalid msg(%s) from client.', [var_export($msg, true)]); + break; + } + + $rc = false; + + do { + try { + $rc = $ipc0->sendmulti([$msg[0], '', $msg[2]], ZMQ::MODE_NOBLOCK); + } catch (ZMQSocketException $e) { + if (4 == $e->getCode()) { + continue; + } + } + + break; + } while (true); + + if (false === $rc) { + \ServerBench\syslog_error( + 'ipc0\'s mq is out of memory, msg(%s) would lose.', + [var_export($msg, true)] + ); + } + } catch (ZMQSocketException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + } + } else { + // unexpected branch. + } + } + } + } + } + + public function __destruct() + { + $this->ipc0_ = null; + $this->ipc1_ = null; + $this->acceptor_ = null; + $this->zctx_ = null; + } +} diff --git a/src/ServerBench/Timer/PeriodicTask.php b/src/ServerBench/Timer/PeriodicTask.php new file mode 100644 index 0000000..f3d96bd --- /dev/null +++ b/src/ServerBench/Timer/PeriodicTask.php @@ -0,0 +1,30 @@ +interval_ = $interval; + parent::__construct((int)(gettimeofday(true) * 1000) + $this->interval_, $cb); + } + + public function run() + { + parent::run(); + } + + public function nextTime() + { + $this->ts += $this->interval_; + return $this; + } +} diff --git a/src/ServerBench/Timer/Task.php b/src/ServerBench/Timer/Task.php new file mode 100644 index 0000000..7746e46 --- /dev/null +++ b/src/ServerBench/Timer/Task.php @@ -0,0 +1,36 @@ +ts = $ts; + $this->cb_ = $cb; + } + + public function run() + { + call_user_func($this->cb_); + } + + public function cancel() + { + $this->canceled_ = true; + } + + public function canceled() + { + return $this->canceled_; + } +} diff --git a/src/ServerBench/Timer/TaskQueue.php b/src/ServerBench/Timer/TaskQueue.php new file mode 100644 index 0000000..26696f3 --- /dev/null +++ b/src/ServerBench/Timer/TaskQueue.php @@ -0,0 +1,16 @@ +ts < $task2->ts ? 1 : ($task1->ts == $task2->ts ? 0 : -1); + } +} diff --git a/src/ServerBench/Timer/Timer.php b/src/ServerBench/Timer/Timer.php new file mode 100644 index 0000000..563d302 --- /dev/null +++ b/src/ServerBench/Timer/Timer.php @@ -0,0 +1,117 @@ +queue_ = new TaskQueue(); + } + + public function isEmpty() + { + return $this->queue_->isEmpty(); + } + + public function runAt($ts, $cb) + { + return $this->runAtMs($ts * 1000, $cb); + } + + public function runAtMs($ts, $cb) + { + $task = new Task($ts, $cb); + $this->queue_->insert($task); + return $task; + } + + public function runAfter($interval, $cb) + { + return $this->runAfterMs($interval * 1000, $cb); + } + + public function runAfterMs($interval, $cb) + { + $task = new Task((int)(gettimeofday(true) * 1000) + $interval, $cb); + $this->queue_->insert($task); + return $task; + } + + public function runEvery($interval, $cb) + { + return $this->runEveryMs($interval * 1000, $cb); + } + + public function runEveryMs($interval, $cb) + { + $task = new PeriodicTask($interval, $cb); + $this->queue_->insert($task); + return $task; + } + + public function nearestTime() + { + return (int)($this->nearestTimeMs() / 1000); + } + + public function nearestTimeMs() + { + return $this->queue_->isEmpty() ? null : $this->queue_->top()->ts; + } + + public function nearestDeltaTime() + { + return (int)($this->nearestDeltaTimeMs() / 1000); + } + + public function nearestDeltaTimeMs() + { + return $this->nearestTimeMs() - (int)(gettimeofday(true) * 1000); + } + + public function execute($max = PHP_INT_MAX) + { + $now = (int)(gettimeofday(true) * 1000); + $i = 1; + + while (!$this->queue_->isEmpty()) { + $task = $this->queue_->top(); + + if ($task->ts <= $now) { + $this->queue_->extract(); + + if (!$task->canceled()) { + $task->run(); + + if ($task instanceof PeriodicTask) { + $this->queue_->insert($task->nextTime()); + } + } + + if (++$i > $max) { + break; + } + } else { + break; + } + } + } + + public function clear() + { + $this->queue_ = new TaskQueue(); + return $this; + } +} diff --git a/src/ServerBench/Worker/Worker.php b/src/ServerBench/Worker/Worker.php new file mode 100644 index 0000000..9593ecc --- /dev/null +++ b/src/ServerBench/Worker/Worker.php @@ -0,0 +1,160 @@ +message_callback_ = $message_callback; + $zctx = new ZMQContext(1, false); + $this->zctx_ = $zctx; + + $ipc0 = new ZMQSocket($zctx, ZMQ::SOCKET_PULL); + $ipc0->setSockOpt(ZMQ::SOCKOPT_RCVHWM, 0); + $ipc0->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + $ipc0->connect($ipcs[0]); + $this->ipc0_ = $ipc0; + + $ipc1 = new ZMQSocket($zctx, ZMQ::SOCKET_PUSH); + $ipc1->setSockOpt(ZMQ::SOCKOPT_SNDHWM, 0); + $ipc1->setSockOpt(ZMQ::SOCKOPT_LINGER, 0); + $ipc1->connect($ipcs[1]); + $this->ipc1_ = $ipc1; + } + + public function setMaxWaitMs($ms) + { + $this->max_wait_ms_ = $ms; + } + + public function run() + { + $ipc0 = $this->ipc0_; + $ipc1 = $this->ipc1_; + + $timer = Timer::getInstance(); + $loop = Loop::getInstance(); + + $poller = new ZMQPoll(); + $poller->add($ipc0, ZMQ::POLL_IN); + + while ($loop->running()) { + // not execute timer too many times + $timer->execute(64); + + $readable = []; + $writable = []; + + try { + if ($timer->isEmpty()) { + $events = $poller->poll($readable, $writable, $this->max_wait_ms_); + } else { + $nearest_delta_ms = $timer->nearestDeltaTimeMs(); + + if ($this->max_wait_ms_ > 0 && $this->max_wait_ms_ < $nearest_delta_ms) { + $real_wait_ms = $this->max_wait_ms_; + } else { + if ($nearest_delta_ms < 0) { + $nearest_delta_ms = 0; + } + + $real_wait_ms = $nearest_delta_ms; + } + + $events = $poller->poll($readable, $writable, $real_wait_ms); + } + + if ($events <= 0) { + continue; + } + } catch (ZMQPollException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + + while (true) { + try { + $msg = $ipc0->recvMulti(ZMQ::MODE_NOBLOCK); + + if (false === $msg) { + // would block + break; + } + + if (count($msg) !== 3 || !empty($msg[1])) { + break; + } + + try { + $reply = call_user_func($this->message_callback_, $msg[2]); + } catch (\Exception $e) { + \ServerBench\syslog_error('caught exception from message_callback: %s', [$e]); + continue; + } + + $rc = false; + + do { + try { + $rc = $ipc1->sendmulti([$msg[0], '', $reply]); + } catch (ZMQPollException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + + break; + } while (true); + + if (false === $rc) { + \ServerBench\syslog_error( + 'replying\'s mq of worker is out of memory, msg(%s) would lose', + [$reply] + ); + } + } catch (ZMQSocketException $e) { + if (4 == $e->getCode()) { + continue; + } + + throw $e; + } + } + } + } + + public function __destruct() + { + $this->ipc0_ = null; + $this->ipc1_ = null; + $this->zctx_ = null; + } +} diff --git a/src/ServerBench/cli/cli.php b/src/ServerBench/cli/cli.php new file mode 100644 index 0000000..8c7c2ba --- /dev/null +++ b/src/ServerBench/cli/cli.php @@ -0,0 +1,257 @@ + 'config:', + 'l:' => 'listen:', + 'd' => 'daemonize', + 'D:' => 'dir:', + 'h' => 'help', + 'n::' => 'worker::', + 'v' => 'version', + 'T:' => 'title:', + 'reload', + 'stop', + 'status', + 'pidfile:', + 'pid:', + 'codec:', + 'app:', + 'ipcs:' +]); + +$not_met = []; + +if (!\ServerBench\Cli\check_requirements($not_met)) { + \ServerBench\syslog_error('requirements is not met.'); + + foreach ($not_met as $item) { + \ServerBench\syslog_error('failed to load extension(%s)', [$item]); + } + + exit(); +} + +if (isset($arguments['version'])) { + \ServerBench\Cli\printf_and_exit("version %s\n", \ServerBench\Cli\get_version()); +} + +if (isset($arguments['help'])) { + \ServerBench\Cli\printf_and_exit("%s\n", \ServerBench\Cli\get_usage()); +} + +$phpbin = $_SERVER['_']; +$script = $_SERVER['PWD'] . '/' . $_SERVER['PHP_SELF']; + +if (isset($arguments['config'])) { + $conf_path = realpath($arguments['config']); + + if (!file_exists($conf_path)) { + \ServerBench\Cli\printf_and_exit("conf(%s) does not exist.\n", $arguments['config']); + } + + $conf = parse_ini_file($conf_path, true); + + foreach ($conf as $k => $v) { + $arguments[$k] = $v; + } +} + +$title = $arguments->get('title', 'serverbench'); +$dir = $arguments->get('dir', getcwd()); + +if (isset($dir)) { + $dir = realpath($dir); + \ServerBench\syslog_info('cd %s', [$dir]); + chdir($dir); +} + +$get_pidarg = function () use ($arguments) { + $pid = $arguments->get('pid'); + + if (!isset($pid)) { + $pidfile = $arguments->get('pidfile', './pid'); + + if (isset($pidfile)) { + $pidfile = realpath($pidfile); + + if (file_exists($pidfile)) { + $pid = file_get_contents($pidfile); + } + } + } + + if (isset($pid)) { + return $pid; + } + + return null; +}; + +if (isset($arguments['reload'])) { + $pid = $get_pidarg(); + + if (!isset($pid)) { + printf_and_exit("no pid found.\n"); + } + + \ServerBench\Cli\reload_server($pid); + exit(); +} + +if (isset($arguments['stop'])) { + $pid = $get_pidarg(); + + if (!isset($pid)) { + \ServerBench\Cli\printf_and_exit("no pid found.\n"); + } + + \ServerBench\Cli\stop_server($pid); + exit(); +} + +if (isset($arguments['status'])) { + $pid = $get_pidarg(); + + if (!isset($pid)) { + \ServerBench\Cli\printf_and_exit("no pid found.\n"); + } + + \ServerBench\Cli\show_status($pid); + exit(); +} + +$listen_addr = $arguments->get('listen'); + +if (!isset($listen_addr)) { + \ServerBench\Cli\printf_and_exit("argument --listen or -l should be set.\n"); +} + +\ServerBench\syslog_info('listen %s', [$listen_addr]); + +$app_path = $arguments->get('app'); + +if (!isset($app_path)) { + \ServerBench\Cli\printf_and_exit("arugment --app should be set.\n"); +} + +$app_path = realpath($app_path); + +if (!file_exists($app_path)) { + \ServerBench\Cli\printf_and_exit("app(%s) does not exist.\n", $app_path); +} + +$ipcs = $arguments->get('ipcs'); + +if (!isset($ipcs)) { + $rand = mt_rand(); + $ipcs = sprintf('ipc://%s/ipc%d_%s_0.sock,ipc://%s/ipc%d_%s_1.sock', $dir, $rand, $title, $dir, $rand, $title); +} + +\ServerBench\syslog_info('app %s', [$app_path]); + +$workers = $arguments->get('workers', 1); +\ServerBench\syslog_info('start workers %d', [$workers]); + +if (isset($arguments['daemonize'])) { + \ServerBench\syslog_info('run as daemon.'); + ProcessUtil::daemon(); +} + +$pid = getmypid(); +\ServerBench\syslog_info('pid of controller is %d', [$pid]); + +$pidfile = $arguments->get('pidfile'); + +if (isset($pidfile)) { + $realpath_pidfile = realpath($pidfile); + + if (file_exists($realpath_pidfile) && file_get_contents($pidfile) != '') { + \ServerBench\syslog_error('there is a running instance already.'); + exit(); + } + + file_put_contents($pidfile, $pid); + + register_shutdown_function(function () use ($pidfile) { + @unlink($pidfile); + }); +} + +ProcessUtil::setTitle($arguments->get('title', 'serverbench')); + +try { + $api = new Api('controller', include($app_path)); + + if (false === $api->init()) { + \ServerBench\syslog_error('app->init(\'controller\') returns false.'); + exit(); + } + + PeriodicGc::enable(300); + + $controller = new Controller(); + $controller->run( + [ + 'groups' => [ + [ + 'proxy' => [ + 'routine' => [ + $phpbin, + [ + '-r', sprintf('require(\'%s\');', __DIR__ . '/start_proxy.php'), + '--', + // __DIR__ . '/start_proxy.php', + '--title', sprintf('%s', $title), + '--listen', $listen_addr, + '--app', $app_path, + '--ipcs', $ipcs + ] + ] + ], + 'worker' => [ + 'routine' => [ + $phpbin, + [ + '-r', sprintf('require(\'%s\');', __DIR__ . '/start_worker.php'), + '--', + // __DIR__ . '/start_worker.php', + '--title', sprintf('%s', $title), + '--app', $app_path, + '--ipcs', $ipcs + ] + ], + 'num' => $workers + ] + ] + ] + ], + $dir + ); + + if (false === $api->fini()) { + \ServerBench\syslog_error('app->fini(\'controller\') returns false.'); + } +} catch (Exception $e) { + \ServerBench\syslog_error('uncaught exception from controller: %s', [$e]); +} diff --git a/src/ServerBench/cli/misc.php b/src/ServerBench/cli/misc.php new file mode 100644 index 0000000..14fba62 --- /dev/null +++ b/src/ServerBench/cli/misc.php @@ -0,0 +1,80 @@ + 'listen:', + 'd' => 'daemonize', + 'D:' => 'dir:', + 'T:' => 'title:', + 'app:', + 'ipcs:' +)); + +if (isset($arguments['dir'])) { + chdir($arguments['dir']); +} + +if (!isset($arguments['listen'])) { + \ServerBench\Cli\printf_and_exit("argument --listen or -l should be set.\n"); +} + +$listen_addr = explode(',', $arguments['listen']); + +if (!isset($arguments['ipcs'])) { + \ServerBench\Cli\printf_and_exit("argument --ipcs should be set.\n"); +} + +$ipcs = explode(',', $arguments['ipcs']); + +if (count($ipcs) != 2) { + \ServerBench\Cli\printf_and_exit("argument --ipcs should be a couple of sock files.\n"); +} + +$api = null; +$app_path = $arguments->get('app'); + +ProcessUtil::setTitle($arguments->get('title', 'serverbench')); + +try { + if (isset($app_path) && file_exists($app_path)) { + $api = new Api('proxy', include($app_path)); + } + + if ($api && false === $api->init()) { + \ServerBench\syslog_error('app->init(\'proxy\') returns false.'); + exit(); + } + + PeriodicGc::enable(300); + + $proxy = new Proxy($listen_addr, $ipcs); + $proxy->run(); + + if ($api && false === $api->fini()) { + \ServerBench\syslog_error('app->fini(\'proxy\') returns false.'); + } +} catch (Exception $e) { + \ServerBench\syslog_error('uncaught exception from proxy: %s', [$e]); +} diff --git a/src/ServerBench/cli/start_worker.php b/src/ServerBench/cli/start_worker.php new file mode 100644 index 0000000..93dbae0 --- /dev/null +++ b/src/ServerBench/cli/start_worker.php @@ -0,0 +1,78 @@ + 'daemonize', + 'D:' => 'dir:', + 'T:' => 'title:', + 'app:', + 'ipcs:' +]); + +$dir = $arguments->get('dir', getcwd()); + +if (isset($dir)) { + chdir($dir); +} + +$app_path = $arguments->get('app'); + +if (!isset($app_path)) { + \ServerBench\Cli\printf_and_exit("arugment --app should be set.\n"); +} + +if (!file_exists($app_path)) { + \ServerBench\Cli\printf_and_exit("app(%s) does not exist.", $app_path); +} + +if (!isset($arguments['ipcs'])) { + \ServerBench\Cli\printf_and_exit("argument --ipcs should be set.\n"); +} + +$ipcs = explode(',', $arguments['ipcs']); + +if (count($ipcs) != 2) { + \ServerBench\Cli\printf_and_exit("argument --ipcs should be a couple of sock files.\n"); +} + +ProcessUtil::setTitle($arguments->get('title', 'serverbench')); + +try { + $api = new Api('worker', include($app_path)); + + if (false === $api->init()) { + \ServerBench\syslog_error('app->init(\'worker\') returns false.'); + exit(); + } + + PeriodicGc::enable(300); + + $worker = new Worker($ipcs, function ($message) use ($api) { + return $api->process($message); + }); + + $worker->run(); + + if (false === $api->fini()) { + \ServerBench\syslog_error('app->fini(\'worker\') returns false.'); + } +} catch (Exception $e) { + \ServerBench\syslog_error('uncaught exception from worker: %s', [$e]); +} diff --git a/src/ServerBench/helpers.php b/src/ServerBench/helpers.php new file mode 100644 index 0000000..bdc0d48 --- /dev/null +++ b/src/ServerBench/helpers.php @@ -0,0 +1,28 @@ +log($level, $message, $context); +} + +function syslog_error($message, $context = []) +{ + Logger\SysLogger::getInstance()->log('error', $message, $context); +} + +function syslog_info($message, $context = []) +{ + Logger\SysLogger::getInstance()->log('info', $message, $context); +} + +function syslog_debug($message, $context = []) +{ + Logger\SysLogger::getInstance()->log('debug', $message, $context); +} diff --git a/test/syslog.php b/test/syslog.php new file mode 100644 index 0000000..d0f76c8 --- /dev/null +++ b/test/syslog.php @@ -0,0 +1,25 @@ +emergency('emergency'); +$console_logger->alert('alert'); +$console_logger->critical('critical'); +$console_logger->error('error'); +$console_logger->warning('warning'); +$console_logger->notice('notice'); +$console_logger->info('info'); +$console_logger->debug('debug'); + +$system_logger = \ServerBench\Logger\SysLogger::getInstance(); +$system_logger->setLogger($console_logger); + +ServerBench\syslog('emergency', 'system emergency'); +ServerBench\syslog('alert', 'system alert'); +ServerBench\syslog('critical', 'system critical'); +ServerBench\syslog('error', 'system error'); +ServerBench\syslog('warning', 'system warning'); +ServerBench\syslog('notice', 'system notice'); +ServerBench\syslog('info', 'system info'); +ServerBench\syslog('debug', 'system debug'); diff --git a/test/timer.php b/test/timer.php new file mode 100644 index 0000000..3b78b50 --- /dev/null +++ b/test/timer.php @@ -0,0 +1,30 @@ +runAfterMs(100, function () { + echo "\nafter 100 ms\n"; +}); + +$timer->runAtMs((int)(millitime()) + 200, function () { + echo "\nat 200 ms\n"; +}); + +$timer->runEveryMs(3000, function () { + echo "\nevery 3000 ms\n"; +}); + +while (!$timer->isEmpty()) { + $timer->execute(); + var_dump($timer->nearestTimeMs() - (int)(gettimeofday(true) * 1000)); + usleep(100000); +} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.0