diff --git a/externals/cldr/cldr_windows_timezones.xml b/externals/cldr/cldr_windows_timezones.xml new file mode 100644 --- /dev/null +++ b/externals/cldr/cldr_windows_timezones.xml @@ -0,0 +1,769 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/timezones/generate-timezone-map.php b/resources/timezones/generate-timezone-map.php new file mode 100755 --- /dev/null +++ b/resources/timezones/generate-timezone-map.php @@ -0,0 +1,46 @@ +#!/usr/bin/env php +windowsZones->mapTimezones->mapZone; +foreach ($zones as $zone) { + $windows_name = (string)$zone['other']; + $target_name = (string)$zone['type']; + + // Ignore the offset-based timezones from the CLDR map, since we handle + // these later. + if (isset($ignore[$windows_name])) { + continue; + } + + // We've already seen this timezone so we don't need to add it to the map + // again. + if (isset($result_map[$windows_name])) { + continue; + } + + $result_map[$windows_name] = $target_name; +} + +asort($result_map); + +echo id(new PhutilJSON()) + ->encodeFormatted($result_map); diff --git a/resources/timezones/windows-timezones.json b/resources/timezones/windows-timezones.json new file mode 100644 --- /dev/null +++ b/resources/timezones/windows-timezones.json @@ -0,0 +1,126 @@ +{ + "Egypt Standard Time": "Africa/Cairo", + "Morocco Standard Time": "Africa/Casablanca", + "South Africa Standard Time": "Africa/Johannesburg", + "W. Central Africa Standard Time": "Africa/Lagos", + "E. Africa Standard Time": "Africa/Nairobi", + "Libya Standard Time": "Africa/Tripoli", + "Namibia Standard Time": "Africa/Windhoek", + "Aleutian Standard Time": "America/Adak", + "Alaskan Standard Time": "America/Anchorage", + "Tocantins Standard Time": "America/Araguaina", + "Paraguay Standard Time": "America/Asuncion", + "Bahia Standard Time": "America/Bahia", + "SA Pacific Standard Time": "America/Bogota", + "Argentina Standard Time": "America/Buenos_Aires", + "Eastern Standard Time (Mexico)": "America/Cancun", + "Venezuela Standard Time": "America/Caracas", + "SA Eastern Standard Time": "America/Cayenne", + "Central Standard Time": "America/Chicago", + "Mountain Standard Time (Mexico)": "America/Chihuahua", + "Central Brazilian Standard Time": "America/Cuiaba", + "Mountain Standard Time": "America/Denver", + "Greenland Standard Time": "America/Godthab", + "Turks And Caicos Standard Time": "America/Grand_Turk", + "Central America Standard Time": "America/Guatemala", + "Atlantic Standard Time": "America/Halifax", + "Cuba Standard Time": "America/Havana", + "US Eastern Standard Time": "America/Indianapolis", + "SA Western Standard Time": "America/La_Paz", + "Pacific Standard Time": "America/Los_Angeles", + "Central Standard Time (Mexico)": "America/Mexico_City", + "Saint Pierre Standard Time": "America/Miquelon", + "Montevideo Standard Time": "America/Montevideo", + "Eastern Standard Time": "America/New_York", + "US Mountain Standard Time": "America/Phoenix", + "Haiti Standard Time": "America/Port-au-Prince", + "Canada Central Standard Time": "America/Regina", + "Pacific SA Standard Time": "America/Santiago", + "E. South America Standard Time": "America/Sao_Paulo", + "Newfoundland Standard Time": "America/St_Johns", + "Pacific Standard Time (Mexico)": "America/Tijuana", + "Central Asia Standard Time": "Asia/Almaty", + "Jordan Standard Time": "Asia/Amman", + "Arabic Standard Time": "Asia/Baghdad", + "Azerbaijan Standard Time": "Asia/Baku", + "SE Asia Standard Time": "Asia/Bangkok", + "Altai Standard Time": "Asia/Barnaul", + "Middle East Standard Time": "Asia/Beirut", + "India Standard Time": "Asia/Calcutta", + "Transbaikal Standard Time": "Asia/Chita", + "Sri Lanka Standard Time": "Asia/Colombo", + "Syria Standard Time": "Asia/Damascus", + "Bangladesh Standard Time": "Asia/Dhaka", + "Arabian Standard Time": "Asia/Dubai", + "West Bank Standard Time": "Asia/Hebron", + "W. Mongolia Standard Time": "Asia/Hovd", + "North Asia East Standard Time": "Asia/Irkutsk", + "Israel Standard Time": "Asia/Jerusalem", + "Afghanistan Standard Time": "Asia/Kabul", + "Russia Time Zone 11": "Asia/Kamchatka", + "Pakistan Standard Time": "Asia/Karachi", + "Nepal Standard Time": "Asia/Katmandu", + "North Asia Standard Time": "Asia/Krasnoyarsk", + "Magadan Standard Time": "Asia/Magadan", + "N. Central Asia Standard Time": "Asia/Novosibirsk", + "Omsk Standard Time": "Asia/Omsk", + "North Korea Standard Time": "Asia/Pyongyang", + "Myanmar Standard Time": "Asia/Rangoon", + "Arab Standard Time": "Asia/Riyadh", + "Sakhalin Standard Time": "Asia/Sakhalin", + "Korea Standard Time": "Asia/Seoul", + "China Standard Time": "Asia/Shanghai", + "Singapore Standard Time": "Asia/Singapore", + "Russia Time Zone 10": "Asia/Srednekolymsk", + "Taipei Standard Time": "Asia/Taipei", + "West Asia Standard Time": "Asia/Tashkent", + "Georgian Standard Time": "Asia/Tbilisi", + "Iran Standard Time": "Asia/Tehran", + "Tokyo Standard Time": "Asia/Tokyo", + "Tomsk Standard Time": "Asia/Tomsk", + "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", + "Vladivostok Standard Time": "Asia/Vladivostok", + "Yakutsk Standard Time": "Asia/Yakutsk", + "Ekaterinburg Standard Time": "Asia/Yekaterinburg", + "Caucasus Standard Time": "Asia/Yerevan", + "Azores Standard Time": "Atlantic/Azores", + "Cape Verde Standard Time": "Atlantic/Cape_Verde", + "Greenwich Standard Time": "Atlantic/Reykjavik", + "Cen. Australia Standard Time": "Australia/Adelaide", + "E. Australia Standard Time": "Australia/Brisbane", + "AUS Central Standard Time": "Australia/Darwin", + "Aus Central W. Standard Time": "Australia/Eucla", + "Tasmania Standard Time": "Australia/Hobart", + "Lord Howe Standard Time": "Australia/Lord_Howe", + "W. Australia Standard Time": "Australia/Perth", + "AUS Eastern Standard Time": "Australia/Sydney", + "Dateline Standard Time": "Etc/GMT+12", + "Astrakhan Standard Time": "Europe/Astrakhan", + "W. Europe Standard Time": "Europe/Berlin", + "GTB Standard Time": "Europe/Bucharest", + "Central Europe Standard Time": "Europe/Budapest", + "E. Europe Standard Time": "Europe/Chisinau", + "Turkey Standard Time": "Europe/Istanbul", + "Kaliningrad Standard Time": "Europe/Kaliningrad", + "FLE Standard Time": "Europe/Kiev", + "GMT Standard Time": "Europe/London", + "Belarus Standard Time": "Europe/Minsk", + "Russian Standard Time": "Europe/Moscow", + "Romance Standard Time": "Europe/Paris", + "Russia Time Zone 3": "Europe/Samara", + "Central European Standard Time": "Europe/Warsaw", + "Mauritius Standard Time": "Indian/Mauritius", + "Samoa Standard Time": "Pacific/Apia", + "New Zealand Standard Time": "Pacific/Auckland", + "Bougainville Standard Time": "Pacific/Bougainville", + "Chatham Islands Standard Time": "Pacific/Chatham", + "Easter Island Standard Time": "Pacific/Easter", + "Fiji Standard Time": "Pacific/Fiji", + "Central Pacific Standard Time": "Pacific/Guadalcanal", + "Hawaiian Standard Time": "Pacific/Honolulu", + "Line Islands Standard Time": "Pacific/Kiritimati", + "Marquesas Standard Time": "Pacific/Marquesas", + "Norfolk Standard Time": "Pacific/Norfolk", + "West Pacific Standard Time": "Pacific/Port_Moresby", + "Tonga Standard Time": "Pacific/Tongatapu" +} diff --git a/scripts/daemon/exec/exec_daemon.php b/scripts/daemon/exec/exec_daemon.php new file mode 100755 --- /dev/null +++ b/scripts/daemon/exec/exec_daemon.php @@ -0,0 +1,131 @@ +#!/usr/bin/env php +setTagline(pht('daemon executor')); +$args->setSynopsis(<<parse( + array( + array( + 'name' => 'trace', + 'help' => pht('Enable debug tracing.'), + ), + 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'), + ), + array( + 'name' => 'daemon', + 'wildcard' => true, + ), + )); + +$trace_memory = $args->getArg('trace-memory'); +$trace_mode = $args->getArg('trace') || $trace_memory; +$verbose = $args->getArg('verbose'); + +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); + +PhutilTypeSpec::checkMap( + $config, + array( + 'log' => 'optional string|null', + 'argv' => 'optional list', + 'load' => 'optional list', + 'down' => 'optional int', + )); + +$log = idx($config, 'log'); + +if ($log) { + ini_set('error_log', $log); + PhutilErrorHandler::setErrorListener(array('PhutilDaemon', 'errorListener')); +} + +$load = idx($config, 'load', array()); +foreach ($load as $library) { + $library = Filesystem::resolvePath($library); + phutil_load_library($library); +} + +PhutilErrorHandler::initialize(); + +$daemon = $args->getArg('daemon'); +if (!$daemon) { + throw new PhutilArgumentUsageException( + pht('Specify which class of daemon to start.')); +} else if (count($daemon) > 1) { + throw new PhutilArgumentUsageException( + pht('Specify exactly one daemon to start.')); +} else { + $daemon = head($daemon); + if (!class_exists($daemon)) { + throw new PhutilArgumentUsageException( + pht( + 'No class "%s" exists in any known library.', + $daemon)); + } else if (!is_subclass_of($daemon, 'PhutilDaemon')) { + throw new PhutilArgumentUsageException( + pht( + 'Class "%s" is not a subclass of "%s".', + $daemon, + 'PhutilDaemon')); + } +} + +$argv = idx($config, 'argv', array()); +$daemon = newv($daemon, array($argv)); + +if ($trace_mode) { + $daemon->setTraceMode(); +} + +if ($trace_memory) { + $daemon->setTraceMemory(); +} + +if ($verbose) { + $daemon->setVerbose(true); +} + +$down_duration = idx($config, 'down'); +if ($down_duration) { + $daemon->setScaledownDuration($down_duration); +} + +$daemon->execute(); diff --git a/scripts/init/lib.php b/scripts/init/lib.php --- a/scripts/init/lib.php +++ b/scripts/init/lib.php @@ -8,10 +8,14 @@ ini_set( 'include_path', $include_path.PATH_SEPARATOR.dirname(__FILE__).'/../../../'); - @include_once 'libphutil/scripts/__init_script__.php'; - if (!@constant('__LIBPHUTIL__')) { - echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ". - "include the parent directory of libphutil/.\n"; + + $ok = @include_once 'arcanist/scripts/init/init-script.php'; + if (!$ok) { + echo + 'FATAL ERROR: Unable to load the "Arcanist" library. '. + 'Put "arcanist/" next to "phabricator/" on disk.'; + echo "\n"; + exit(1); } diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5623,6 +5623,11 @@ 'PhutilCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilCodeSnippetContextFreeGrammar.php', 'PhutilConsoleSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 'PhutilContextFreeGrammar' => 'infrastructure/lipsum/PhutilContextFreeGrammar.php', + 'PhutilDaemon' => 'infrastructure/daemon/PhutilDaemon.php', + 'PhutilDaemonHandle' => 'infrastructure/daemon/PhutilDaemonHandle.php', + 'PhutilDaemonOverseer' => 'infrastructure/daemon/PhutilDaemonOverseer.php', + 'PhutilDaemonOverseerModule' => 'infrastructure/daemon/PhutilDaemonOverseerModule.php', + 'PhutilDaemonPool' => 'infrastructure/daemon/PhutilDaemonPool.php', 'PhutilDefaultSyntaxHighlighter' => 'infrastructure/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php', 'PhutilDefaultSyntaxHighlighterEngine' => 'infrastructure/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'infrastructure/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php', @@ -12522,6 +12527,11 @@ 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilConsoleSyntaxHighlighter' => 'Phobject', 'PhutilContextFreeGrammar' => 'Phobject', + 'PhutilDaemon' => 'Phobject', + 'PhutilDaemonHandle' => 'Phobject', + 'PhutilDaemonOverseer' => 'Phobject', + 'PhutilDaemonOverseerModule' => 'Phobject', + 'PhutilDaemonPool' => 'Phobject', 'PhutilDefaultSyntaxHighlighter' => 'Phobject', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', diff --git a/src/applications/calendar/parser/ics/PhutilICSParser.php b/src/applications/calendar/parser/ics/PhutilICSParser.php --- a/src/applications/calendar/parser/ics/PhutilICSParser.php +++ b/src/applications/calendar/parser/ics/PhutilICSParser.php @@ -849,8 +849,8 @@ ); // Load the map of Windows timezones. - $root_path = dirname(phutil_get_library_root('phutil')); - $windows_path = $root_path.'/resources/timezones/windows_timezones.json'; + $root_path = dirname(phutil_get_library_root('phabricator')); + $windows_path = $root_path.'/resources/timezones/windows-timezones.json'; $windows_data = Filesystem::readFile($windows_path); $windows_zones = phutil_json_decode($windows_data); diff --git a/src/infrastructure/daemon/PhutilDaemon.php b/src/infrastructure/daemon/PhutilDaemon.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/daemon/PhutilDaemon.php @@ -0,0 +1,393 @@ +shouldExit()) { + * if (work_available()) { + * $this->willBeginWork(); + * do_work(); + * $this->sleep(0); + * } else { + * $this->willBeginIdle(); + * $this->sleep(1); + * } + * } + * + * In particular, call @{method:willBeginWork} before becoming busy, and + * @{method:willBeginIdle} when no work is available. If the daemon is launched + * into an autoscale pool, this will cause the pool to automatically scale up + * when busy and down when idle. + * + * See @{class:PhutilHighIntensityIntervalDaemon} for an example of a simple + * autoscaling daemon. + * + * Launching a daemon which does not make these callbacks into an autoscale + * pool will have no effect. + * + * @task overseer Communicating With the Overseer + * @task autoscale Autoscaling Daemon Pools + */ +abstract class PhutilDaemon extends Phobject { + + const MESSAGETYPE_STDOUT = 'stdout'; + const MESSAGETYPE_HEARTBEAT = 'heartbeat'; + const MESSAGETYPE_BUSY = 'busy'; + const MESSAGETYPE_IDLE = 'idle'; + const MESSAGETYPE_DOWN = 'down'; + const MESSAGETYPE_HIBERNATE = 'hibernate'; + + const WORKSTATE_BUSY = 'busy'; + const WORKSTATE_IDLE = 'idle'; + + private $argv; + private $traceMode; + private $traceMemory; + private $verbose; + private $notifyReceived; + private $inGracefulShutdown; + private $workState = null; + private $idleSince = null; + private $scaledownDuration; + + final public function setVerbose($verbose) { + $this->verbose = $verbose; + return $this; + } + + final public function getVerbose() { + return $this->verbose; + } + + final public function setScaledownDuration($scaledown_duration) { + $this->scaledownDuration = $scaledown_duration; + return $this; + } + + final public function getScaledownDuration() { + return $this->scaledownDuration; + } + + final public function __construct(array $argv) { + $this->argv = $argv; + + $router = PhutilSignalRouter::getRouter(); + $handler_key = 'daemon.term'; + if (!$router->getHandler($handler_key)) { + $handler = new PhutilCallbackSignalHandler( + SIGTERM, + __CLASS__.'::onTermSignal'); + $router->installHandler($handler_key, $handler); + } + + pcntl_signal(SIGINT, array($this, 'onGracefulSignal')); + pcntl_signal(SIGUSR2, array($this, 'onNotifySignal')); + + // Without discard mode, this consumes unbounded amounts of memory. Keep + // memory bounded. + PhutilServiceProfiler::getInstance()->enableDiscardMode(); + + $this->beginStdoutCapture(); + } + + final public function __destruct() { + $this->endStdoutCapture(); + } + + final public function stillWorking() { + $this->emitOverseerMessage(self::MESSAGETYPE_HEARTBEAT, null); + + if ($this->traceMemory) { + $daemon = get_class($this); + fprintf( + STDERR, + "%s %s %s\n", + '', + $daemon, + pht( + 'Memory Usage: %s KB', + new PhutilNumber(memory_get_usage() / 1024, 1))); + } + } + + final public function shouldExit() { + return $this->inGracefulShutdown; + } + + final protected function shouldHibernate($duration) { + // Don't hibernate if we don't have very long to sleep. + if ($duration < 30) { + return false; + } + + // Never hibernate if we're part of a pool and could scale down instead. + // We only hibernate the last process to drop the pool size to zero. + if ($this->getScaledownDuration()) { + return false; + } + + // Don't hibernate for too long. + $duration = min($duration, phutil_units('3 minutes in seconds')); + + $this->emitOverseerMessage( + self::MESSAGETYPE_HIBERNATE, + array( + 'duration' => $duration, + )); + + $this->log( + pht( + 'Preparing to hibernate for %s second(s).', + new PhutilNumber($duration))); + + return true; + } + + final protected function sleep($duration) { + $this->notifyReceived = false; + $this->willSleep($duration); + $this->stillWorking(); + + $scale_down = $this->getScaledownDuration(); + + $max_sleep = 60; + if ($scale_down) { + $max_sleep = min($max_sleep, $scale_down); + } + + if ($scale_down) { + if ($this->workState == self::WORKSTATE_IDLE) { + $dur = $this->getIdleDuration(); + $this->log(pht('Idle for %s seconds.', $dur)); + } + } + + while ($duration > 0 && + !$this->notifyReceived && + !$this->shouldExit()) { + + // If this is an autoscaling clone and we've been idle for too long, + // we're going to scale the pool down by exiting and not restarting. The + // DOWN message tells the overseer that we don't want to be restarted. + if ($scale_down) { + if ($this->workState == self::WORKSTATE_IDLE) { + if ($this->idleSince && ($this->idleSince + $scale_down < time())) { + $this->inGracefulShutdown = true; + $this->emitOverseerMessage(self::MESSAGETYPE_DOWN, null); + $this->log( + pht( + 'Daemon was idle for more than %s second(s), '. + 'scaling pool down.', + new PhutilNumber($scale_down))); + break; + } + } + } + + sleep(min($duration, $max_sleep)); + $duration -= $max_sleep; + $this->stillWorking(); + } + } + + protected function willSleep($duration) { + return; + } + + public static function onTermSignal($signo) { + self::didCatchSignal($signo); + } + + final protected function getArgv() { + return $this->argv; + } + + final public function execute() { + $this->willRun(); + $this->run(); + } + + abstract protected function run(); + + final public function setTraceMemory() { + $this->traceMemory = true; + return $this; + } + + final public function getTraceMemory() { + return $this->traceMemory; + } + + final public function setTraceMode() { + $this->traceMode = true; + PhutilServiceProfiler::installEchoListener(); + PhutilConsole::getConsole()->getServer()->setEnableLog(true); + $this->didSetTraceMode(); + return $this; + } + + final public function getTraceMode() { + return $this->traceMode; + } + + final public function onGracefulSignal($signo) { + self::didCatchSignal($signo); + $this->inGracefulShutdown = true; + } + + final public function onNotifySignal($signo) { + self::didCatchSignal($signo); + $this->notifyReceived = true; + $this->onNotify($signo); + } + + protected function onNotify($signo) { + // This is a hook for subclasses. + } + + protected function willRun() { + // This is a hook for subclasses. + } + + protected function didSetTraceMode() { + // This is a hook for subclasses. + } + + final protected function log($message) { + if ($this->verbose) { + $daemon = get_class($this); + fprintf(STDERR, "%s %s %s\n", '', $daemon, $message); + } + } + + private static function didCatchSignal($signo) { + $signame = phutil_get_signal_name($signo); + fprintf( + STDERR, + "%s Caught signal %s (%s).\n", + '', + $signo, + $signame); + } + + +/* -( Communicating With the Overseer )------------------------------------ */ + + + private function beginStdoutCapture() { + ob_start(array($this, 'didReceiveStdout'), 2); + } + + private function endStdoutCapture() { + ob_end_flush(); + } + + public function didReceiveStdout($data) { + if (!strlen($data)) { + return ''; + } + + return $this->encodeOverseerMessage(self::MESSAGETYPE_STDOUT, $data); + } + + private function encodeOverseerMessage($type, $data) { + $structure = array($type); + + if ($data !== null) { + $structure[] = $data; + } + + return json_encode($structure)."\n"; + } + + private function emitOverseerMessage($type, $data) { + $this->endStdoutCapture(); + echo $this->encodeOverseerMessage($type, $data); + $this->beginStdoutCapture(); + } + + public static function errorListener($event, $value, array $metadata) { + // If the caller has redirected the error log to a file, PHP won't output + // messages to stderr, so the overseer can't capture them. Install a + // listener which just echoes errors to stderr, so the overseer is always + // aware of errors. + + $console = PhutilConsole::getConsole(); + $message = idx($metadata, 'default_message'); + + if ($message) { + $console->writeErr("%s\n", $message); + } + if (idx($metadata, 'trace')) { + $trace = PhutilErrorHandler::formatStacktrace($metadata['trace']); + $console->writeErr("%s\n", $trace); + } + } + + +/* -( Autoscaling )-------------------------------------------------------- */ + + + /** + * Prepare to become busy. This may autoscale the pool up. + * + * This notifies the overseer that the daemon has become busy. If daemons + * that are part of an autoscale pool are continuously busy for a prolonged + * period of time, the overseer may scale up the pool. + * + * @return this + * @task autoscale + */ + protected function willBeginWork() { + if ($this->workState != self::WORKSTATE_BUSY) { + $this->workState = self::WORKSTATE_BUSY; + $this->idleSince = null; + $this->emitOverseerMessage(self::MESSAGETYPE_BUSY, null); + } + + return $this; + } + + + /** + * Prepare to idle. This may autoscale the pool down. + * + * This notifies the overseer that the daemon is no longer busy. If daemons + * that are part of an autoscale pool are idle for a prolonged period of + * time, they may exit to scale the pool down. + * + * @return this + * @task autoscale + */ + protected function willBeginIdle() { + if ($this->workState != self::WORKSTATE_IDLE) { + $this->workState = self::WORKSTATE_IDLE; + $this->idleSince = time(); + $this->emitOverseerMessage(self::MESSAGETYPE_IDLE, null); + } + + return $this; + } + + protected function getIdleDuration() { + if (!$this->idleSince) { + return null; + } + + $now = time(); + return ($now - $this->idleSince); + } + +} diff --git a/src/infrastructure/daemon/PhutilDaemonHandle.php b/src/infrastructure/daemon/PhutilDaemonHandle.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/daemon/PhutilDaemonHandle.php @@ -0,0 +1,506 @@ + + } + + public static function newFromConfig(array $config) { + PhutilTypeSpec::checkMap( + $config, + array( + 'class' => 'string', + 'argv' => 'optional list', + 'load' => 'optional list', + 'log' => 'optional string|null', + 'down' => 'optional int', + )); + + $config = $config + array( + 'argv' => array(), + 'load' => array(), + 'log' => null, + 'down' => 15, + ); + + $daemon = new self(); + $daemon->properties = $config; + $daemon->daemonID = $daemon->generateDaemonID(); + + return $daemon; + } + + public function setDaemonPool(PhutilDaemonPool $daemon_pool) { + $this->pool = $daemon_pool; + return $this; + } + + public function getDaemonPool() { + return $this->pool; + } + + public function getBusyEpoch() { + return $this->busyEpoch; + } + + public function getDaemonClass() { + return $this->getProperty('class'); + } + + private function getProperty($key) { + return idx($this->properties, $key); + } + + public function setCommandLineArguments(array $arguments) { + $this->argv = $arguments; + return $this; + } + + public function getCommandLineArguments() { + return $this->argv; + } + + public function getDaemonArguments() { + return $this->getProperty('argv'); + } + + public function didLaunch() { + $this->restartAt = time(); + $this->shouldSendExitEvent = true; + + $this->dispatchEvent( + self::EVENT_DID_LAUNCH, + array( + 'argv' => $this->getCommandLineArguments(), + 'explicitArgv' => $this->getDaemonArguments(), + )); + + return $this; + } + + public function isRunning() { + return (bool)$this->future; + } + + public function isHibernating() { + return + !$this->isRunning() && + !$this->isDone() && + $this->hibernating; + } + + public function wakeFromHibernation() { + if (!$this->isHibernating()) { + return $this; + } + + $this->logMessage( + 'WAKE', + pht( + 'Process is being awakened from hibernation.')); + + $this->restartAt = time(); + $this->update(); + + return $this; + } + + public function isDone() { + return (!$this->shouldRestart && !$this->isRunning()); + } + + public function getFuture() { + return $this->future; + } + + public function update() { + if (!$this->isRunning()) { + if (!$this->shouldRestart) { + return; + } + if (!$this->restartAt || (time() < $this->restartAt)) { + return; + } + if ($this->shouldShutdown) { + return; + } + $this->startDaemonProcess(); + } + + $future = $this->future; + + $result = null; + if ($future->isReady()) { + $result = $future->resolve(); + } + + list($stdout, $stderr) = $future->read(); + $future->discardBuffers(); + + if (strlen($stdout)) { + $this->didReadStdout($stdout); + } + + $stderr = trim($stderr); + if (strlen($stderr)) { + foreach (phutil_split_lines($stderr, false) as $line) { + $this->logMessage('STDE', $line); + } + } + + if ($result !== null) { + list($err) = $result; + + if ($err) { + $this->logMessage('FAIL', pht('Process exited with error %s.', $err)); + } else { + $this->logMessage('DONE', pht('Process exited normally.')); + } + + $this->future = null; + + if ($this->shouldShutdown) { + $this->restartAt = null; + } else { + $this->scheduleRestart(); + } + } + + $this->updateHeartbeatEvent(); + $this->updateHangDetection(); + } + + private function updateHeartbeatEvent() { + if ($this->heartbeat > time()) { + return; + } + + $this->heartbeat = time() + $this->getHeartbeatEventFrequency(); + $this->dispatchEvent(self::EVENT_DID_HEARTBEAT); + } + + private function updateHangDetection() { + if (!$this->isRunning()) { + return; + } + + if (time() > $this->deadline) { + $this->logMessage('HANG', pht('Hang detected. Restarting process.')); + $this->annihilateProcessGroup(); + $this->scheduleRestart(); + } + } + + private function scheduleRestart() { + // Wait a minimum of a few sceconds before restarting, but we may wait + // longer if the daemon has initiated hibernation. + $default_restart = time() + self::getWaitBeforeRestart(); + if ($default_restart >= $this->restartAt) { + $this->restartAt = $default_restart; + } + + $this->logMessage( + 'WAIT', + pht( + 'Waiting %s second(s) to restart process.', + new PhutilNumber($this->restartAt - time()))); + } + + /** + * Generate a unique ID for this daemon. + * + * @return string A unique daemon ID. + */ + private function generateDaemonID() { + return substr(getmypid().':'.Filesystem::readRandomCharacters(12), 0, 12); + } + + public function getDaemonID() { + return $this->daemonID; + } + + public function getPID() { + return $this->pid; + } + + private function getCaptureBufferSize() { + return 65535; + } + + private function getRequiredHeartbeatFrequency() { + return 86400; + } + + public static function getWaitBeforeRestart() { + return 5; + } + + public static function getHeartbeatEventFrequency() { + return 120; + } + + private function getKillDelay() { + return 3; + } + + private function getDaemonCWD() { + $root = dirname(phutil_get_library_root('phabricator')); + return $root.'/scripts/daemon/exec/'; + } + + private function newExecFuture() { + $class = $this->getDaemonClass(); + $argv = $this->getCommandLineArguments(); + $buffer_size = $this->getCaptureBufferSize(); + + // NOTE: PHP implements proc_open() by running 'sh -c'. On most systems this + // is bash, but on Ubuntu it's dash. When you proc_open() using bash, you + // get one new process (the command you ran). When you proc_open() using + // dash, you get two new processes: the command you ran and a parent + // "dash -c" (or "sh -c") process. This means that the child process's PID + // is actually the 'dash' PID, not the command's PID. To avoid this, use + // 'exec' to replace the shell process with the real process; without this, + // the child will call posix_getppid(), be given the pid of the 'sh -c' + // process, and send it SIGUSR1 to keepalive which will terminate it + // immediately. We also won't be able to do process group management because + // the shell process won't properly posix_setsid() so the pgid of the child + // won't be meaningful. + + $config = $this->properties; + unset($config['class']); + $config = phutil_json_encode($config); + + return id(new ExecFuture('exec ./exec_daemon.php %s %Ls', $class, $argv)) + ->setCWD($this->getDaemonCWD()) + ->setStdoutSizeLimit($buffer_size) + ->setStderrSizeLimit($buffer_size) + ->write($config); + } + + /** + * Dispatch an event to event listeners. + * + * @param string Event type. + * @param dict Event parameters. + * @return void + */ + private function dispatchEvent($type, array $params = array()) { + $data = array( + 'id' => $this->getDaemonID(), + 'daemonClass' => $this->getDaemonClass(), + 'childPID' => $this->getPID(), + ) + $params; + + $event = new PhutilEvent($type, $data); + + try { + PhutilEventEngine::dispatchEvent($event); + } catch (Exception $ex) { + phlog($ex); + } + } + + private function annihilateProcessGroup() { + $pid = $this->getPID(); + + $pgid = posix_getpgid($pid); + if ($pid && $pgid) { + posix_kill(-$pgid, SIGTERM); + sleep($this->getKillDelay()); + posix_kill(-$pgid, SIGKILL); + $this->pid = null; + } + } + + private function startDaemonProcess() { + $this->logMessage('INIT', pht('Starting process.')); + + $this->deadline = time() + $this->getRequiredHeartbeatFrequency(); + $this->heartbeat = time() + self::getHeartbeatEventFrequency(); + $this->stdoutBuffer = ''; + $this->hibernating = false; + + $this->future = $this->newExecFuture(); + $this->future->start(); + + $this->pid = $this->future->getPID(); + } + + private function didReadStdout($data) { + $this->stdoutBuffer .= $data; + while (true) { + $pos = strpos($this->stdoutBuffer, "\n"); + if ($pos === false) { + break; + } + $message = substr($this->stdoutBuffer, 0, $pos); + $this->stdoutBuffer = substr($this->stdoutBuffer, $pos + 1); + + try { + $structure = phutil_json_decode($message); + } catch (PhutilJSONParserException $ex) { + $structure = array(); + } + + switch (idx($structure, 0)) { + case PhutilDaemon::MESSAGETYPE_STDOUT: + $this->logMessage('STDO', idx($structure, 1)); + break; + case PhutilDaemon::MESSAGETYPE_HEARTBEAT: + $this->deadline = time() + $this->getRequiredHeartbeatFrequency(); + break; + case PhutilDaemon::MESSAGETYPE_BUSY: + if (!$this->busyEpoch) { + $this->busyEpoch = time(); + } + break; + case PhutilDaemon::MESSAGETYPE_IDLE: + $this->busyEpoch = null; + break; + case PhutilDaemon::MESSAGETYPE_DOWN: + // The daemon is exiting because it doesn't have enough work and it + // is trying to scale the pool down. We should not restart it. + $this->shouldRestart = false; + $this->shouldShutdown = true; + break; + case PhutilDaemon::MESSAGETYPE_HIBERNATE: + $config = idx($structure, 1); + $duration = (int)idx($config, 'duration', 0); + $this->restartAt = time() + $duration; + $this->hibernating = true; + $this->busyEpoch = null; + $this->logMessage( + 'ZZZZ', + pht( + 'Process is preparing to hibernate for %s second(s).', + new PhutilNumber($duration))); + break; + default: + // If we can't parse this or it isn't a message we understand, just + // emit the raw message. + $this->logMessage('STDO', pht(' %s', $message)); + break; + } + } + } + + public function didReceiveNotifySignal($signo) { + $pid = $this->getPID(); + if ($pid) { + posix_kill($pid, $signo); + } + } + + public function didReceiveReloadSignal($signo) { + $signame = phutil_get_signal_name($signo); + if ($signame) { + $sigmsg = pht( + 'Reloading in response to signal %d (%s).', + $signo, + $signame); + } else { + $sigmsg = pht( + 'Reloading in response to signal %d.', + $signo); + } + + $this->logMessage('RELO', $sigmsg, $signo); + + // This signal means "stop the current process gracefully, then launch + // a new identical process once it exits". This can be used to update + // daemons after code changes (the new processes will run the new code) + // without aborting any running tasks. + + // We SIGINT the daemon but don't set the shutdown flag, so it will + // naturally be restarted after it exits, as though it had exited after an + // unhandled exception. + + posix_kill($this->getPID(), SIGINT); + } + + public function didReceiveGracefulSignal($signo) { + $this->shouldShutdown = true; + $this->shouldRestart = false; + + $signame = phutil_get_signal_name($signo); + if ($signame) { + $sigmsg = pht( + 'Graceful shutdown in response to signal %d (%s).', + $signo, + $signame); + } else { + $sigmsg = pht( + 'Graceful shutdown in response to signal %d.', + $signo); + } + + $this->logMessage('DONE', $sigmsg, $signo); + + posix_kill($this->getPID(), SIGINT); + } + + public function didReceiveTerminateSignal($signo) { + $this->shouldShutdown = true; + $this->shouldRestart = false; + + $signame = phutil_get_signal_name($signo); + if ($signame) { + $sigmsg = pht( + 'Shutting down in response to signal %s (%s).', + $signo, + $signame); + } else { + $sigmsg = pht('Shutting down in response to signal %s.', $signo); + } + + $this->logMessage('EXIT', $sigmsg, $signo); + $this->annihilateProcessGroup(); + } + + private function logMessage($type, $message, $context = null) { + $this->getDaemonPool()->logMessage($type, $message, $context); + + $this->dispatchEvent( + self::EVENT_DID_LOG, + array( + 'type' => $type, + 'message' => $message, + 'context' => $context, + )); + } + + public function didExit() { + if ($this->shouldSendExitEvent) { + $this->dispatchEvent(self::EVENT_WILL_EXIT); + $this->shouldSendExitEvent = false; + } + + return $this; + } + +} diff --git a/src/infrastructure/daemon/PhutilDaemonOverseer.php b/src/infrastructure/daemon/PhutilDaemonOverseer.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/daemon/PhutilDaemonOverseer.php @@ -0,0 +1,405 @@ +enableDiscardMode(); + + $args = new PhutilArgumentParser($argv); + $args->setTagline(pht('daemon overseer')); + $args->setSynopsis(<<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(); + + while (true) { + if ($this->shouldReloadDaemons()) { + $this->didReceiveSignal(SIGHUP); + } + + $futures = array(); + + $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) { + $futures[] = $future; + } + + if ($pool->getDaemons()) { + $running_pools = true; + } + } + + $this->updateMemory(); + + $this->waitForDaemonFutures($futures); + + if (!$futures && !$running_pools) { + if ($this->shouldShutdown()) { + break; + } + } + } + + exit($this->err); + } + + + private function waitForDaemonFutures(array $futures) { + assert_instances_of($futures, 'ExecFuture'); + + if ($futures) { + // TODO: This only wakes if any daemons actually exit. It would be a bit + // cleaner to wait on any I/O with Channels. + $iter = id(new FutureIterator($futures)) + ->setUpdateInterval(1); + foreach ($iter as $future) { + break; + } + } else { + if (!$this->shouldShutdown()) { + sleep(1); + } + } + } + + 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; + } + +} diff --git a/src/infrastructure/daemon/PhutilDaemonOverseerModule.php b/src/infrastructure/daemon/PhutilDaemonOverseerModule.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/daemon/PhutilDaemonOverseerModule.php @@ -0,0 +1,71 @@ +setAncestorClass(__CLASS__) + ->execute(); + } + + + /** + * Throttle checks from executing too often. + * + * If you throttle a check like this, it will only execute once every 2.5 + * seconds: + * + * if ($this->shouldThrottle('some.check', 2.5)) { + * return; + * } + * + * @param string Throttle key. + * @param float Duration in seconds. + * @return bool True to throttle the check. + */ + protected function shouldThrottle($name, $duration) { + $throttle = idx($this->throttles, $name, 0); + $now = microtime(true); + + // If not enough time has elapsed, throttle the check. + $elapsed = ($now - $throttle); + if ($elapsed < $duration) { + return true; + } + + // Otherwise, mark the current time as the last time we ran the check, + // then let it continue. + $this->throttles[$name] = $now; + + return false; + } + +} diff --git a/src/infrastructure/daemon/PhutilDaemonPool.php b/src/infrastructure/daemon/PhutilDaemonPool.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/daemon/PhutilDaemonPool.php @@ -0,0 +1,360 @@ + + } + + public static function newFromConfig(array $config) { + PhutilTypeSpec::checkMap( + $config, + array( + 'class' => 'string', + 'label' => 'string', + 'argv' => 'optional list', + 'load' => 'optional list', + 'log' => 'optional string|null', + 'pool' => 'optional int', + 'up' => 'optional int', + 'down' => 'optional int', + 'reserve' => 'optional int|float', + )); + + $config = $config + array( + 'argv' => array(), + 'load' => array(), + 'log' => null, + 'pool' => 1, + 'up' => 2, + 'down' => 15, + 'reserve' => 0, + ); + + $pool = new self(); + $pool->properties = $config; + + return $pool; + } + + public function setOverseer(PhutilDaemonOverseer $overseer) { + $this->overseer = $overseer; + return $this; + } + + public function getOverseer() { + return $this->overseer; + } + + public function setCommandLineArguments(array $arguments) { + $this->commandLineArguments = $arguments; + return $this; + } + + public function getCommandLineArguments() { + return $this->commandLineArguments; + } + + private function shouldShutdown() { + return $this->inShutdown; + } + + private function newDaemon() { + $config = $this->properties; + + if (count($this->daemons)) { + $down_duration = $this->getPoolScaledownDuration(); + } else { + // TODO: For now, never scale pools down to 0. + $down_duration = 0; + } + + $forced_config = array( + 'down' => $down_duration, + ); + + $config = $forced_config + $config; + + $config = array_select_keys( + $config, + array( + 'class', + 'log', + 'load', + 'argv', + 'down', + )); + + $daemon = PhutilDaemonHandle::newFromConfig($config) + ->setDaemonPool($this) + ->setCommandLineArguments($this->getCommandLineArguments()); + + $daemon_id = $daemon->getDaemonID(); + $this->daemons[$daemon_id] = $daemon; + + $daemon->didLaunch(); + + return $daemon; + } + + public function getDaemons() { + return $this->daemons; + } + + public function getFutures() { + $futures = array(); + foreach ($this->getDaemons() as $daemon) { + $future = $daemon->getFuture(); + if ($future) { + $futures[] = $future; + } + } + + return $futures; + } + + public function didReceiveSignal($signal, $signo) { + switch ($signal) { + case PhutilDaemonOverseer::SIGNAL_GRACEFUL: + case PhutilDaemonOverseer::SIGNAL_TERMINATE: + $this->inShutdown = true; + break; + } + + foreach ($this->getDaemons() as $daemon) { + switch ($signal) { + case PhutilDaemonOverseer::SIGNAL_NOTIFY: + $daemon->didReceiveNotifySignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_RELOAD: + $daemon->didReceiveReloadSignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_GRACEFUL: + $daemon->didReceiveGracefulSignal($signo); + break; + case PhutilDaemonOverseer::SIGNAL_TERMINATE: + $daemon->didReceiveTerminateSignal($signo); + break; + default: + throw new Exception( + pht( + 'Unknown signal "%s" ("%d").', + $signal, + $signo)); + } + } + } + + public function getPoolLabel() { + return $this->getPoolProperty('label'); + } + + public function getPoolMaximumSize() { + return $this->getPoolProperty('pool'); + } + + public function getPoolScaleupDuration() { + return $this->getPoolProperty('up'); + } + + public function getPoolScaledownDuration() { + return $this->getPoolProperty('down'); + } + + public function getPoolMemoryReserve() { + return $this->getPoolProperty('reserve'); + } + + public function getPoolDaemonClass() { + return $this->getPoolProperty('class'); + } + + private function getPoolProperty($key) { + return idx($this->properties, $key); + } + + public function updatePool() { + $daemons = $this->getDaemons(); + + foreach ($daemons as $key => $daemon) { + $daemon->update(); + + if ($daemon->isDone()) { + $daemon->didExit(); + + unset($this->daemons[$key]); + + if ($this->shouldShutdown()) { + $this->logMessage( + 'DOWN', + pht( + 'Pool "%s" is exiting, with %s daemon(s) remaining.', + $this->getPoolLabel(), + new PhutilNumber(count($this->daemons)))); + } else { + $this->logMessage( + 'POOL', + pht( + 'Autoscale pool "%s" scaled down to %s daemon(s).', + $this->getPoolLabel(), + new PhutilNumber(count($this->daemons)))); + } + } + } + + $this->updateAutoscale(); + } + + public function isHibernating() { + foreach ($this->getDaemons() as $daemon) { + if (!$daemon->isHibernating()) { + return false; + } + } + + return true; + } + + public function wakeFromHibernation() { + if (!$this->isHibernating()) { + return $this; + } + + $this->logMessage( + 'WAKE', + pht( + 'Autoscale pool "%s" is being awakened from hibernation.', + $this->getPoolLabel())); + + $did_wake_daemons = false; + foreach ($this->getDaemons() as $daemon) { + if ($daemon->isHibernating()) { + $daemon->wakeFromHibernation(); + $did_wake_daemons = true; + } + } + + if (!$did_wake_daemons) { + // TODO: Pools currently can't scale down to 0 daemons, but we should + // scale up immediately here once they can. + } + + $this->updatePool(); + + return $this; + } + + private function updateAutoscale() { + if ($this->shouldShutdown()) { + return; + } + + // Don't try to autoscale more than once per second. This mostly stops the + // logs from getting flooded in verbose mode. + $now = time(); + if ($this->lastAutoscaleUpdate >= $now) { + return; + } + $this->lastAutoscaleUpdate = $now; + + $daemons = $this->getDaemons(); + + // If this pool is already at the maximum size, we can't launch any new + // daemons. + $max_size = $this->getPoolMaximumSize(); + if (count($daemons) >= $max_size) { + $this->logMessage( + 'POOL', + pht( + 'Autoscale pool "%s" already at maximum size (%s of %s).', + $this->getPoolLabel(), + new PhutilNumber(count($daemons)), + new PhutilNumber($max_size))); + return; + } + + $scaleup_duration = $this->getPoolScaleupDuration(); + + foreach ($daemons as $daemon) { + $busy_epoch = $daemon->getBusyEpoch(); + // If any daemons haven't started work yet, don't scale the pool up. + if (!$busy_epoch) { + $this->logMessage( + 'POOL', + pht( + 'Autoscale pool "%s" has an idle daemon, declining to scale.', + $this->getPoolLabel())); + return; + } + + // If any daemons started work very recently, wait a little while + // to scale the pool up. + $busy_for = ($now - $busy_epoch); + if ($busy_for < $scaleup_duration) { + $this->logMessage( + 'POOL', + pht( + 'Autoscale pool "%s" has not been busy long enough to scale up '. + '(busy for %s of %s seconds).', + $this->getPoolLabel(), + new PhutilNumber($busy_for), + new PhutilNumber($scaleup_duration))); + return; + } + } + + // If we have a configured memory reserve for this pool, it tells us that + // we should not scale up unless there's at least that much memory left + // on the system (for example, a reserve of 0.25 means that 25% of system + // memory must be free to autoscale). + + // Note that the first daemon is exempt: we'll always launch at least one + // daemon, regardless of any memory reservation. + if (count($daemons)) { + $reserve = $this->getPoolMemoryReserve(); + if ($reserve) { + // On some systems this may be slightly more expensive than other + // checks, so we only do it once we're prepared to scale up. + $memory = PhutilSystem::getSystemMemoryInformation(); + $free_ratio = ($memory['free'] / $memory['total']); + + // If we don't have enough free memory, don't scale. + if ($free_ratio <= $reserve) { + $this->logMessage( + 'POOL', + pht( + 'Autoscale pool "%s" does not have enough free memory to '. + 'scale up (%s free of %s reserved).', + $this->getPoolLabel(), + new PhutilNumber($free_ratio, 3), + new PhutilNumber($reserve, 3))); + return; + } + } + } + + $this->logMessage( + 'AUTO', + pht( + 'Scaling pool "%s" up to %s daemon(s).', + $this->getPoolLabel(), + new PhutilNumber(count($daemons) + 1))); + + $this->newDaemon(); + } + + public function logMessage($type, $message, $context = null) { + return $this->getOverseer()->logMessage($type, $message, $context); + } + +} diff --git a/src/view/widget/AphrontStackTraceView.php b/src/view/widget/AphrontStackTraceView.php --- a/src/view/widget/AphrontStackTraceView.php +++ b/src/view/widget/AphrontStackTraceView.php @@ -19,7 +19,6 @@ $callsigns = array( 'arcanist' => 'ARC', - 'phutil' => 'PHU', 'phabricator' => 'P', ); diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php --- a/support/startup/PhabricatorStartup.php +++ b/support/startup/PhabricatorStartup.php @@ -205,16 +205,13 @@ 'include_path', $libraries_root.PATH_SEPARATOR.ini_get('include_path')); - @include_once $root.'libphutil/src/__phutil_library_init__.php'; - if (!@constant('__LIBPHUTIL__')) { + $ok = @include_once $root.'arcanist/src/init/init-library.php'; + if (!$ok) { self::didFatal( - "Unable to load libphutil. Put libphutil/ next to phabricator/, or ". - "update your PHP 'include_path' to include the parent directory of ". - "libphutil/."); + 'Unable to load the "Arcanist" library. Put "arcanist/" next to '. + '"phabricator/" on disk.'); } - phutil_load_library('arcanist/src'); - // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories).