mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-03 18:38:27 +01:00
1a59cae743
Summary: Depends on D21053. Ref T11968. Three things have changed: - Overseers can no longer use FutureIterator to continue execution of an arbitrary list of futures from any state. Use FuturePool instead. - Same with repository daemons. - Probably (?) fix an API change in the Harbormaster exec future. Test Plan: - Ran "bin/phd debug task" and "bin/phd debug pull", no longer saw Future-management related errors. - The Harbormaster future is easiest to test by just seeing if production works once this change is deployed there. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11968 Differential Revision: https://secure.phabricator.com/D21054
410 lines
9.7 KiB
PHP
410 lines
9.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Oversees a daemon and restarts it if it fails.
|
|
*
|
|
* @task signals Signal Handling
|
|
*/
|
|
final class PhutilDaemonOverseer extends Phobject {
|
|
|
|
private $argv;
|
|
private static $instance;
|
|
|
|
private $config;
|
|
private $pools = array();
|
|
private $traceMode;
|
|
private $traceMemory;
|
|
private $daemonize;
|
|
private $log;
|
|
private $libraries = array();
|
|
private $modules = array();
|
|
private $verbose;
|
|
private $startEpoch;
|
|
private $autoscale = array();
|
|
private $autoscaleConfig = array();
|
|
|
|
const SIGNAL_NOTIFY = 'signal/notify';
|
|
const SIGNAL_RELOAD = 'signal/reload';
|
|
const SIGNAL_GRACEFUL = 'signal/graceful';
|
|
const SIGNAL_TERMINATE = 'signal/terminate';
|
|
|
|
private $err = 0;
|
|
private $inAbruptShutdown;
|
|
private $inGracefulShutdown;
|
|
|
|
private $futurePool;
|
|
|
|
public function __construct(array $argv) {
|
|
PhutilServiceProfiler::getInstance()->enableDiscardMode();
|
|
|
|
$args = new PhutilArgumentParser($argv);
|
|
$args->setTagline(pht('daemon overseer'));
|
|
$args->setSynopsis(<<<EOHELP
|
|
**launch_daemon.php** [__options__] __daemon__
|
|
Launch and oversee an instance of __daemon__.
|
|
EOHELP
|
|
);
|
|
$args->parseStandardArguments();
|
|
$args->parse(
|
|
array(
|
|
array(
|
|
'name' => 'trace-memory',
|
|
'help' => pht('Enable debug memory tracing.'),
|
|
),
|
|
array(
|
|
'name' => 'verbose',
|
|
'help' => pht('Enable verbose activity logging.'),
|
|
),
|
|
array(
|
|
'name' => 'label',
|
|
'short' => 'l',
|
|
'param' => 'label',
|
|
'help' => pht(
|
|
'Optional process label. Makes "%s" nicer, no behavioral effects.',
|
|
'ps'),
|
|
),
|
|
));
|
|
$argv = array();
|
|
|
|
if ($args->getArg('trace')) {
|
|
$this->traceMode = true;
|
|
$argv[] = '--trace';
|
|
}
|
|
|
|
if ($args->getArg('trace-memory')) {
|
|
$this->traceMode = true;
|
|
$this->traceMemory = true;
|
|
$argv[] = '--trace-memory';
|
|
}
|
|
$verbose = $args->getArg('verbose');
|
|
if ($verbose) {
|
|
$this->verbose = true;
|
|
$argv[] = '--verbose';
|
|
}
|
|
|
|
$label = $args->getArg('label');
|
|
if ($label) {
|
|
$argv[] = '-l';
|
|
$argv[] = $label;
|
|
}
|
|
|
|
$this->argv = $argv;
|
|
|
|
if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
|
|
fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");
|
|
}
|
|
$config = @file_get_contents('php://stdin');
|
|
$config = id(new PhutilJSONParser())->parse($config);
|
|
|
|
$this->libraries = idx($config, 'load');
|
|
$this->log = idx($config, 'log');
|
|
$this->daemonize = idx($config, 'daemonize');
|
|
|
|
$this->config = $config;
|
|
|
|
if (self::$instance) {
|
|
throw new Exception(
|
|
pht('You may not instantiate more than one Overseer per process.'));
|
|
}
|
|
|
|
self::$instance = $this;
|
|
|
|
$this->startEpoch = time();
|
|
|
|
if (!idx($config, 'daemons')) {
|
|
throw new PhutilArgumentUsageException(
|
|
pht('You must specify at least one daemon to start!'));
|
|
}
|
|
|
|
if ($this->log) {
|
|
// NOTE: Now that we're committed to daemonizing, redirect the error
|
|
// log if we have a `--log` parameter. Do this at the last moment
|
|
// so as many setup issues as possible are surfaced.
|
|
ini_set('error_log', $this->log);
|
|
}
|
|
|
|
if ($this->daemonize) {
|
|
// We need to get rid of these or the daemon will hang when we TERM it
|
|
// waiting for something to read the buffers. TODO: Learn how unix works.
|
|
fclose(STDOUT);
|
|
fclose(STDERR);
|
|
ob_start();
|
|
|
|
$pid = pcntl_fork();
|
|
if ($pid === -1) {
|
|
throw new Exception(pht('Unable to fork!'));
|
|
} else if ($pid) {
|
|
exit(0);
|
|
}
|
|
|
|
$sid = posix_setsid();
|
|
if ($sid <= 0) {
|
|
throw new Exception(pht('Failed to create new process session!'));
|
|
}
|
|
}
|
|
|
|
$this->logMessage(
|
|
'OVER',
|
|
pht(
|
|
'Started new daemon overseer (with PID "%s").',
|
|
getmypid()));
|
|
|
|
$this->modules = PhutilDaemonOverseerModule::getAllModules();
|
|
|
|
$this->installSignalHandlers();
|
|
}
|
|
|
|
public function addLibrary($library) {
|
|
$this->libraries[] = $library;
|
|
return $this;
|
|
}
|
|
|
|
public function run() {
|
|
$this->createDaemonPools();
|
|
|
|
$future_pool = $this->getFuturePool();
|
|
|
|
while (true) {
|
|
if ($this->shouldReloadDaemons()) {
|
|
$this->didReceiveSignal(SIGHUP);
|
|
}
|
|
|
|
$running_pools = false;
|
|
foreach ($this->getDaemonPools() as $pool) {
|
|
$pool->updatePool();
|
|
|
|
if (!$this->shouldShutdown()) {
|
|
if ($pool->isHibernating()) {
|
|
if ($this->shouldWakePool($pool)) {
|
|
$pool->wakeFromHibernation();
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($pool->getFutures() as $future) {
|
|
$future_pool->addFuture($future);
|
|
}
|
|
|
|
if ($pool->getDaemons()) {
|
|
$running_pools = true;
|
|
}
|
|
}
|
|
|
|
$this->updateMemory();
|
|
|
|
if ($future_pool->hasFutures()) {
|
|
$future_pool->resolve();
|
|
} else {
|
|
if (!$this->shouldShutdown()) {
|
|
sleep(1);
|
|
}
|
|
}
|
|
|
|
if (!$future_pool->hasFutures() && !$running_pools) {
|
|
if ($this->shouldShutdown()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
exit($this->err);
|
|
}
|
|
|
|
private function getFuturePool() {
|
|
if (!$this->futurePool) {
|
|
$pool = new FuturePool();
|
|
|
|
// TODO: This only wakes if any daemons actually exit, or 1 second
|
|
// passes. It would be a bit cleaner to wait on any I/O, but Futures
|
|
// currently can't do that.
|
|
|
|
$pool->getIteratorTemplate()
|
|
->setUpdateInterval(1);
|
|
|
|
$this->futurePool = $pool;
|
|
}
|
|
return $this->futurePool;
|
|
}
|
|
|
|
private function createDaemonPools() {
|
|
$configs = $this->config['daemons'];
|
|
|
|
$forced_options = array(
|
|
'load' => $this->libraries,
|
|
'log' => $this->log,
|
|
);
|
|
|
|
foreach ($configs as $config) {
|
|
$config = $forced_options + $config;
|
|
|
|
$pool = PhutilDaemonPool::newFromConfig($config)
|
|
->setOverseer($this)
|
|
->setCommandLineArguments($this->argv);
|
|
|
|
$this->pools[] = $pool;
|
|
}
|
|
}
|
|
|
|
private function getDaemonPools() {
|
|
return $this->pools;
|
|
}
|
|
|
|
private function updateMemory() {
|
|
if (!$this->traceMemory) {
|
|
return;
|
|
}
|
|
|
|
$this->logMessage(
|
|
'RAMS',
|
|
pht(
|
|
'Overseer Memory Usage: %s KB',
|
|
new PhutilNumber(memory_get_usage() / 1024, 1)));
|
|
}
|
|
|
|
public function logMessage($type, $message, $context = null) {
|
|
$always_log = false;
|
|
switch ($type) {
|
|
case 'OVER':
|
|
case 'SGNL':
|
|
case 'PIDF':
|
|
$always_log = true;
|
|
break;
|
|
}
|
|
|
|
if ($always_log || $this->traceMode || $this->verbose) {
|
|
error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message);
|
|
}
|
|
}
|
|
|
|
|
|
/* -( Signal Handling )---------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* @task signals
|
|
*/
|
|
private function installSignalHandlers() {
|
|
$signals = array(
|
|
SIGUSR2,
|
|
SIGHUP,
|
|
SIGINT,
|
|
SIGTERM,
|
|
);
|
|
|
|
foreach ($signals as $signal) {
|
|
pcntl_signal($signal, array($this, 'didReceiveSignal'));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @task signals
|
|
*/
|
|
public function didReceiveSignal($signo) {
|
|
$this->logMessage(
|
|
'SGNL',
|
|
pht(
|
|
'Overseer ("%d") received signal %d ("%s").',
|
|
getmypid(),
|
|
$signo,
|
|
phutil_get_signal_name($signo)));
|
|
|
|
switch ($signo) {
|
|
case SIGUSR2:
|
|
$signal_type = self::SIGNAL_NOTIFY;
|
|
break;
|
|
case SIGHUP:
|
|
$signal_type = self::SIGNAL_RELOAD;
|
|
break;
|
|
case SIGINT:
|
|
// If we receive SIGINT more than once, interpret it like SIGTERM.
|
|
if ($this->inGracefulShutdown) {
|
|
return $this->didReceiveSignal(SIGTERM);
|
|
}
|
|
|
|
$this->inGracefulShutdown = true;
|
|
$signal_type = self::SIGNAL_GRACEFUL;
|
|
break;
|
|
case SIGTERM:
|
|
// If we receive SIGTERM more than once, terminate abruptly.
|
|
$this->err = 128 + $signo;
|
|
if ($this->inAbruptShutdown) {
|
|
exit($this->err);
|
|
}
|
|
|
|
$this->inAbruptShutdown = true;
|
|
$signal_type = self::SIGNAL_TERMINATE;
|
|
break;
|
|
default:
|
|
throw new Exception(
|
|
pht(
|
|
'Signal handler called with unknown signal type ("%d")!',
|
|
$signo));
|
|
}
|
|
|
|
foreach ($this->getDaemonPools() as $pool) {
|
|
$pool->didReceiveSignal($signal_type, $signo);
|
|
}
|
|
}
|
|
|
|
|
|
/* -( Daemon Modules )----------------------------------------------------- */
|
|
|
|
|
|
private function getModules() {
|
|
return $this->modules;
|
|
}
|
|
|
|
private function shouldReloadDaemons() {
|
|
$modules = $this->getModules();
|
|
|
|
$should_reload = false;
|
|
foreach ($modules as $module) {
|
|
try {
|
|
// NOTE: Even if one module tells us to reload, we call the method on
|
|
// each module anyway to make calls a little more predictable.
|
|
|
|
if ($module->shouldReloadDaemons()) {
|
|
$this->logMessage(
|
|
'RELO',
|
|
pht(
|
|
'Reloading daemons (triggered by overseer module "%s").',
|
|
get_class($module)));
|
|
$should_reload = true;
|
|
}
|
|
} catch (Exception $ex) {
|
|
phlog($ex);
|
|
}
|
|
}
|
|
|
|
return $should_reload;
|
|
}
|
|
|
|
private function shouldWakePool(PhutilDaemonPool $pool) {
|
|
$modules = $this->getModules();
|
|
|
|
$should_wake = false;
|
|
foreach ($modules as $module) {
|
|
try {
|
|
if ($module->shouldWakePool($pool)) {
|
|
$this->logMessage(
|
|
'WAKE',
|
|
pht(
|
|
'Waking pool "%s" (triggered by overseer module "%s").',
|
|
$pool->getPoolLabel(),
|
|
get_class($module)));
|
|
$should_wake = true;
|
|
}
|
|
} catch (Exception $ex) {
|
|
phlog($ex);
|
|
}
|
|
}
|
|
|
|
return $should_wake;
|
|
}
|
|
|
|
private function shouldShutdown() {
|
|
return $this->inGracefulShutdown || $this->inAbruptShutdown;
|
|
}
|
|
|
|
}
|