Path: blob/master/src/infrastructure/daemon/PhutilDaemonOverseer.php
12241 views
<?php12/**3* Oversees a daemon and restarts it if it fails.4*5* @task signals Signal Handling6*/7final class PhutilDaemonOverseer extends Phobject {89private $argv;10private static $instance;1112private $config;13private $pools = array();14private $traceMode;15private $traceMemory;16private $daemonize;17private $log;18private $libraries = array();19private $modules = array();20private $verbose;21private $startEpoch;22private $autoscale = array();23private $autoscaleConfig = array();2425const SIGNAL_NOTIFY = 'signal/notify';26const SIGNAL_RELOAD = 'signal/reload';27const SIGNAL_GRACEFUL = 'signal/graceful';28const SIGNAL_TERMINATE = 'signal/terminate';2930private $err = 0;31private $inAbruptShutdown;32private $inGracefulShutdown;3334private $futurePool;3536public function __construct(array $argv) {37PhutilServiceProfiler::getInstance()->enableDiscardMode();3839$args = new PhutilArgumentParser($argv);40$args->setTagline(pht('daemon overseer'));41$args->setSynopsis(<<<EOHELP42**launch_daemon.php** [__options__] __daemon__43Launch and oversee an instance of __daemon__.44EOHELP45);46$args->parseStandardArguments();47$args->parse(48array(49array(50'name' => 'trace-memory',51'help' => pht('Enable debug memory tracing.'),52),53array(54'name' => 'verbose',55'help' => pht('Enable verbose activity logging.'),56),57array(58'name' => 'label',59'short' => 'l',60'param' => 'label',61'help' => pht(62'Optional process label. Makes "%s" nicer, no behavioral effects.',63'ps'),64),65));66$argv = array();6768if ($args->getArg('trace')) {69$this->traceMode = true;70$argv[] = '--trace';71}7273if ($args->getArg('trace-memory')) {74$this->traceMode = true;75$this->traceMemory = true;76$argv[] = '--trace-memory';77}78$verbose = $args->getArg('verbose');79if ($verbose) {80$this->verbose = true;81$argv[] = '--verbose';82}8384$label = $args->getArg('label');85if ($label) {86$argv[] = '-l';87$argv[] = $label;88}8990$this->argv = $argv;9192if (function_exists('posix_isatty') && posix_isatty(STDIN)) {93fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");94}95$config = @file_get_contents('php://stdin');96$config = id(new PhutilJSONParser())->parse($config);9798$this->libraries = idx($config, 'load');99$this->log = idx($config, 'log');100$this->daemonize = idx($config, 'daemonize');101102$this->config = $config;103104if (self::$instance) {105throw new Exception(106pht('You may not instantiate more than one Overseer per process.'));107}108109self::$instance = $this;110111$this->startEpoch = time();112113if (!idx($config, 'daemons')) {114throw new PhutilArgumentUsageException(115pht('You must specify at least one daemon to start!'));116}117118if ($this->log) {119// NOTE: Now that we're committed to daemonizing, redirect the error120// log if we have a `--log` parameter. Do this at the last moment121// so as many setup issues as possible are surfaced.122ini_set('error_log', $this->log);123}124125if ($this->daemonize) {126// We need to get rid of these or the daemon will hang when we TERM it127// waiting for something to read the buffers. TODO: Learn how unix works.128fclose(STDOUT);129fclose(STDERR);130ob_start();131132$pid = pcntl_fork();133if ($pid === -1) {134throw new Exception(pht('Unable to fork!'));135} else if ($pid) {136exit(0);137}138139$sid = posix_setsid();140if ($sid <= 0) {141throw new Exception(pht('Failed to create new process session!'));142}143}144145$this->logMessage(146'OVER',147pht(148'Started new daemon overseer (with PID "%s").',149getmypid()));150151$this->modules = PhutilDaemonOverseerModule::getAllModules();152153$this->installSignalHandlers();154}155156public function addLibrary($library) {157$this->libraries[] = $library;158return $this;159}160161public function run() {162$this->createDaemonPools();163164$future_pool = $this->getFuturePool();165166while (true) {167if ($this->shouldReloadDaemons()) {168$this->didReceiveSignal(SIGHUP);169}170171$running_pools = false;172foreach ($this->getDaemonPools() as $pool) {173$pool->updatePool();174175if (!$this->shouldShutdown()) {176if ($pool->isHibernating()) {177if ($this->shouldWakePool($pool)) {178$pool->wakeFromHibernation();179}180}181}182183if ($pool->getDaemons()) {184$running_pools = true;185}186}187188$this->updateMemory();189190if ($future_pool->hasFutures()) {191$future_pool->resolve();192} else {193if (!$this->shouldShutdown()) {194sleep(1);195}196}197198if (!$future_pool->hasFutures() && !$running_pools) {199if ($this->shouldShutdown()) {200break;201}202}203}204205exit($this->err);206}207208public function addFutureToPool(Future $future) {209$this->getFuturePool()->addFuture($future);210return $this;211}212213private function getFuturePool() {214if (!$this->futurePool) {215$pool = new FuturePool();216217// TODO: This only wakes if any daemons actually exit, or 1 second218// passes. It would be a bit cleaner to wait on any I/O, but Futures219// currently can't do that.220221$pool->getIteratorTemplate()222->setUpdateInterval(1);223224$this->futurePool = $pool;225}226return $this->futurePool;227}228229private function createDaemonPools() {230$configs = $this->config['daemons'];231232$forced_options = array(233'load' => $this->libraries,234'log' => $this->log,235);236237foreach ($configs as $config) {238$config = $forced_options + $config;239240$pool = PhutilDaemonPool::newFromConfig($config)241->setOverseer($this)242->setCommandLineArguments($this->argv);243244$this->pools[] = $pool;245}246}247248private function getDaemonPools() {249return $this->pools;250}251252private function updateMemory() {253if (!$this->traceMemory) {254return;255}256257$this->logMessage(258'RAMS',259pht(260'Overseer Memory Usage: %s KB',261new PhutilNumber(memory_get_usage() / 1024, 1)));262}263264public function logMessage($type, $message, $context = null) {265$always_log = false;266switch ($type) {267case 'OVER':268case 'SGNL':269case 'PIDF':270$always_log = true;271break;272}273274if ($always_log || $this->traceMode || $this->verbose) {275error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message);276}277}278279280/* -( Signal Handling )---------------------------------------------------- */281282283/**284* @task signals285*/286private function installSignalHandlers() {287$signals = array(288SIGUSR2,289SIGHUP,290SIGINT,291SIGTERM,292);293294foreach ($signals as $signal) {295pcntl_signal($signal, array($this, 'didReceiveSignal'));296}297}298299300/**301* @task signals302*/303public function didReceiveSignal($signo) {304$this->logMessage(305'SGNL',306pht(307'Overseer ("%d") received signal %d ("%s").',308getmypid(),309$signo,310phutil_get_signal_name($signo)));311312switch ($signo) {313case SIGUSR2:314$signal_type = self::SIGNAL_NOTIFY;315break;316case SIGHUP:317$signal_type = self::SIGNAL_RELOAD;318break;319case SIGINT:320// If we receive SIGINT more than once, interpret it like SIGTERM.321if ($this->inGracefulShutdown) {322return $this->didReceiveSignal(SIGTERM);323}324325$this->inGracefulShutdown = true;326$signal_type = self::SIGNAL_GRACEFUL;327break;328case SIGTERM:329// If we receive SIGTERM more than once, terminate abruptly.330$this->err = 128 + $signo;331if ($this->inAbruptShutdown) {332exit($this->err);333}334335$this->inAbruptShutdown = true;336$signal_type = self::SIGNAL_TERMINATE;337break;338default:339throw new Exception(340pht(341'Signal handler called with unknown signal type ("%d")!',342$signo));343}344345foreach ($this->getDaemonPools() as $pool) {346$pool->didReceiveSignal($signal_type, $signo);347}348}349350351/* -( Daemon Modules )----------------------------------------------------- */352353354private function getModules() {355return $this->modules;356}357358private function shouldReloadDaemons() {359$modules = $this->getModules();360361$should_reload = false;362foreach ($modules as $module) {363try {364// NOTE: Even if one module tells us to reload, we call the method on365// each module anyway to make calls a little more predictable.366367if ($module->shouldReloadDaemons()) {368$this->logMessage(369'RELO',370pht(371'Reloading daemons (triggered by overseer module "%s").',372get_class($module)));373$should_reload = true;374}375} catch (Exception $ex) {376phlog($ex);377}378}379380return $should_reload;381}382383private function shouldWakePool(PhutilDaemonPool $pool) {384$modules = $this->getModules();385386$should_wake = false;387foreach ($modules as $module) {388try {389if ($module->shouldWakePool($pool)) {390$this->logMessage(391'WAKE',392pht(393'Waking pool "%s" (triggered by overseer module "%s").',394$pool->getPoolLabel(),395get_class($module)));396$should_wake = true;397}398} catch (Exception $ex) {399phlog($ex);400}401}402403return $should_wake;404}405406private function shouldShutdown() {407return $this->inGracefulShutdown || $this->inAbruptShutdown;408}409410}411412413