diff --git a/externals/cldr/cldr_windows_timezones.xml b/externals/cldr/cldr_windows_timezones.xml
new file mode 100644
index 0000000000..47b689d8af
--- /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
index 0000000000..77ee76c52b
--- /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
index 0000000000..7a287b6c55
--- /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
index 0000000000..af53131612
--- /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
index 431d2c1373..266697e96e 100644
--- a/scripts/init/lib.php
+++ b/scripts/init/lib.php
@@ -8,10 +8,14 @@ function init_phabricator_script(array $options) {
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
index 9ddf5d01a4..0728a08273 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -5623,6 +5623,11 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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
index 015bea601c..1cd7261e12 100644
--- a/src/applications/calendar/parser/ics/PhutilICSParser.php
+++ b/src/applications/calendar/parser/ics/PhutilICSParser.php
@@ -849,8 +849,8 @@ final class PhutilICSParser extends Phobject {
);
// 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
index 0000000000..701b3d7a27
--- /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
index 0000000000..5030f5330c
--- /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
index 0000000000..44e0c41384
--- /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
index 0000000000..3e2cdaad3e
--- /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
index 0000000000..50b22289a8
--- /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
index edb805af8f..ff8cac43bc 100644
--- a/src/view/widget/AphrontStackTraceView.php
+++ b/src/view/widget/AphrontStackTraceView.php
@@ -19,7 +19,6 @@ final class AphrontStackTraceView extends AphrontView {
$callsigns = array(
'arcanist' => 'ARC',
- 'phutil' => 'PHU',
'phabricator' => 'P',
);
diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
index c166162310..da9e3d8bf9 100644
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -205,16 +205,13 @@ final class PhabricatorStartup {
'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).