Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
index 01216d5ffb..ef6315bfc2 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
@@ -1,183 +1,191 @@
<?php
final class PhabricatorDaemonLogViewController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$log = id(new PhabricatorDaemonLogQuery())
->setViewer($user)
->withIDs(array($this->id))
->setAllowStatusWrites(true)
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$events = id(new PhabricatorDaemonLogEvent())->loadAllWhere(
'logID = %d ORDER BY id DESC LIMIT 1000',
$log->getID());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Daemon %s', $log->getID()));
$header = id(new PHUIHeaderView())
->setHeader($log->getDaemon());
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE);
$status = $log->getStatus();
switch ($status) {
case PhabricatorDaemonLog::STATUS_UNKNOWN:
$tag->setBackgroundColor(PHUITagView::COLOR_ORANGE);
$tag->setName(pht('Unknown'));
break;
case PhabricatorDaemonLog::STATUS_RUNNING:
$tag->setBackgroundColor(PHUITagView::COLOR_GREEN);
$tag->setName(pht('Running'));
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$tag->setBackgroundColor(PHUITagView::COLOR_RED);
$tag->setName(pht('Dead'));
break;
case PhabricatorDaemonLog::STATUS_WAIT:
$tag->setBackgroundColor(PHUITagView::COLOR_BLUE);
$tag->setName(pht('Waiting'));
break;
+ case PhabricatorDaemonLog::STATUS_EXITING:
+ $tag->setBackgroundColor(PHUITagView::COLOR_YELLOW);
+ $tag->setName(pht('Exiting'));
+ break;
case PhabricatorDaemonLog::STATUS_EXITED:
$tag->setBackgroundColor(PHUITagView::COLOR_GREY);
$tag->setName(pht('Exited'));
break;
}
$header->addTag($tag);
$properties = $this->buildPropertyListView($log);
$event_view = id(new PhabricatorDaemonLogEventsView())
->setUser($user)
->setEvents($events);
$event_panel = new AphrontPanelView();
$event_panel->setNoBackground();
$event_panel->appendChild($event_view);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$event_panel,
),
array(
'title' => pht('Daemon Log'),
'device' => false,
));
}
private function buildPropertyListView(PhabricatorDaemonLog $daemon) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$id = $daemon->getID();
$c_epoch = $daemon->getDateCreated();
$u_epoch = $daemon->getDateModified();
$unknown_time = PhabricatorDaemonLogQuery::getTimeUntilUnknown();
$dead_time = PhabricatorDaemonLogQuery::getTimeUntilDead();
$wait_time = PhutilDaemonOverseer::RESTART_WAIT;
$details = null;
$status = $daemon->getStatus();
switch ($status) {
case PhabricatorDaemonLog::STATUS_RUNNING:
$details = pht(
'This daemon is running normally and reported a status update '.
'recently (within %s).',
phutil_format_relative_time($unknown_time));
break;
case PhabricatorDaemonLog::STATUS_UNKNOWN:
$details = pht(
'This daemon has not reported a status update recently (within %s). '.
'It may have exited abruptly. After %s, it will be presumed dead.',
phutil_format_relative_time($unknown_time),
phutil_format_relative_time($dead_time));
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$details = pht(
'This daemon did not report a status update for %s. It is '.
'presumed dead. Usually, this indicates that the daemon was '.
'killed or otherwise exited abruptly with an error. You may '.
'need to restart it.',
phutil_format_relative_time($dead_time));
break;
case PhabricatorDaemonLog::STATUS_WAIT:
$details = pht(
'This daemon is running normally and reported a status update '.
'recently (within %s). However, it encountered an error while '.
'doing work and is waiting a little while (%s) to resume '.
'processing. After encountering an error, daemons wait before '.
'resuming work to avoid overloading services.',
phutil_format_relative_time($unknown_time),
phutil_format_relative_time($wait_time));
break;
+ case PhabricatorDaemonLog::STATUS_EXITING:
+ $details = pht(
+ 'This daemon is shutting down gracefully.');
+ break;
case PhabricatorDaemonLog::STATUS_EXITED:
$details = pht(
'This daemon exited normally and is no longer running.');
break;
}
$view->addProperty(pht('Status Details'), $details);
$view->addProperty(pht('Daemon Class'), $daemon->getDaemon());
$view->addProperty(pht('Host'), $daemon->getHost());
$view->addProperty(pht('PID'), $daemon->getPID());
$view->addProperty(pht('Started'), phabricator_datetime($c_epoch, $viewer));
$view->addProperty(
pht('Seen'),
pht(
'%s ago (%s)',
phutil_format_relative_time(time() - $u_epoch),
phabricator_datetime($u_epoch, $viewer)));
$argv = $daemon->getArgv();
if (is_array($argv)) {
$argv = implode("\n", $argv);
}
$view->addProperty(
pht('Argv'),
phutil_tag(
'textarea',
array(
'style' => 'width: 100%; height: 12em;',
),
$argv));
$view->addProperty(
pht('View Full Logs'),
phutil_tag(
'tt',
array(),
"phabricator/ $ ./bin/phd log {$id}"));
return $view;
}
}
diff --git a/src/applications/daemon/event/PhabricatorDaemonEventListener.php b/src/applications/daemon/event/PhabricatorDaemonEventListener.php
index 537d20e621..0fc78daa97 100644
--- a/src/applications/daemon/event/PhabricatorDaemonEventListener.php
+++ b/src/applications/daemon/event/PhabricatorDaemonEventListener.php
@@ -1,105 +1,116 @@
<?php
final class PhabricatorDaemonEventListener extends PhabricatorEventListener {
private $daemons = array();
public function register() {
$this->listen(PhutilDaemonOverseer::EVENT_DID_LAUNCH);
$this->listen(PhutilDaemonOverseer::EVENT_DID_LOG);
$this->listen(PhutilDaemonOverseer::EVENT_DID_HEARTBEAT);
+ $this->listen(PhutilDaemonOverseer::EVENT_WILL_GRACEFUL);
$this->listen(PhutilDaemonOverseer::EVENT_WILL_EXIT);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhutilDaemonOverseer::EVENT_DID_LAUNCH:
$this->handleLaunchEvent($event);
break;
case PhutilDaemonOverseer::EVENT_DID_HEARTBEAT:
$this->handleHeartbeatEvent($event);
break;
case PhutilDaemonOverseer::EVENT_DID_LOG:
$this->handleLogEvent($event);
break;
+ case PhutilDaemonOverseer::EVENT_WILL_GRACEFUL:
+ $this->handleGracefulEvent($event);
+ break;
case PhutilDaemonOverseer::EVENT_WILL_EXIT:
$this->handleExitEvent($event);
break;
}
}
private function handleLaunchEvent(PhutilEvent $event) {
$id = $event->getValue('id');
$daemon = id(new PhabricatorDaemonLog())
->setDaemon($event->getValue('daemonClass'))
->setHost(php_uname('n'))
->setPID(getmypid())
->setStatus(PhabricatorDaemonLog::STATUS_RUNNING)
->setArgv($event->getValue('argv'))
->setExplicitArgv($event->getValue('explicitArgv'))
->save();
$this->daemons[$id] = $daemon;
}
private function handleHeartbeatEvent(PhutilEvent $event) {
$daemon = $this->getDaemon($event->getValue('id'));
// Just update the timestamp.
$daemon->save();
}
private function handleLogEvent(PhutilEvent $event) {
$daemon = $this->getDaemon($event->getValue('id'));
// TODO: This is a bit awkward for historical reasons, clean it up after
// removing Conduit.
$message = $event->getValue('message');
$context = $event->getValue('context');
if (strlen($context) && $context !== $message) {
$message = "({$context}) {$message}";
}
$type = $event->getValue('type');
$message = phutil_utf8ize($message);
id(new PhabricatorDaemonLogEvent())
->setLogID($daemon->getID())
->setLogType($type)
->setMessage((string)$message)
->setEpoch(time())
->save();
switch ($type) {
case 'WAIT':
$current_status = PhabricatorDaemonLog::STATUS_WAIT;
break;
default:
$current_status = PhabricatorDaemonLog::STATUS_RUNNING;
break;
}
if ($current_status !== $daemon->getStatus()) {
$daemon->setStatus($current_status)->save();
}
}
+ private function handleGracefulEvent(PhutilEvent $event) {
+ $id = $event->getValue('id');
+
+ $daemon = $this->getDaemon($id);
+ $daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITING)->save();
+ }
+
private function handleExitEvent(PhutilEvent $event) {
$id = $event->getValue('id');
$daemon = $this->getDaemon($id);
$daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITED)->save();
unset($this->daemons[$id]);
}
private function getDaemon($id) {
if (isset($this->daemons[$id])) {
return $this->daemons[$id];
}
throw new Exception("No such daemon '{$id}'!");
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
index 052770a12f..8aa8eac0d8 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
@@ -1,23 +1,34 @@
<?php
final class PhabricatorDaemonManagementRestartWorkflow
extends PhabricatorDaemonManagementWorkflow {
public function didConstruct() {
$this
->setName('restart')
->setSynopsis(
pht(
'Stop, then start the standard daemon loadout.'))
- ->setArguments(array());
+ ->setArguments(
+ array(
+ array(
+ 'name' => 'graceful',
+ 'param' => 'seconds',
+ 'help' => pht(
+ 'Grace period for daemons to attempt a clean shutdown, in '.
+ 'seconds. Defaults to __15__ seconds.'),
+ 'default' => 15,
+ ),
+ ));
}
public function execute(PhutilArgumentParser $args) {
- $err = $this->executeStopCommand(array());
+ $graceful = $args->getArg('graceful');
+ $err = $this->executeStopCommand(array(), $graceful);
if ($err) {
return $err;
}
return $this->executeStartCommand();
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
index 6b96664b8b..3b690af58c 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
@@ -1,27 +1,36 @@
<?php
final class PhabricatorDaemonManagementStopWorkflow
extends PhabricatorDaemonManagementWorkflow {
public function didConstruct() {
$this
->setName('stop')
->setSynopsis(
pht(
'Stop all running daemons, or specific daemons identified by PIDs. '.
'Use **phd status** to find PIDs.'))
->setArguments(
array(
+ array(
+ 'name' => 'graceful',
+ 'param' => 'seconds',
+ 'help' => pht(
+ 'Grace period for daemons to attempt a clean shutdown, in '.
+ 'seconds. Defaults to __15__ seconds.'),
+ 'default' => 15,
+ ),
array(
'name' => 'pids',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$pids = $args->getArg('pids');
- return $this->executeStopCommand($pids);
+ $graceful = $args->getArg('graceful');
+ return $this->executeStopCommand($pids, $graceful);
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
index 8690a80a4d..a9253e7a93 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
@@ -1,383 +1,407 @@
<?php
abstract class PhabricatorDaemonManagementWorkflow
extends PhabricatorManagementWorkflow {
protected final function loadAvailableDaemonClasses() {
$loader = new PhutilSymbolLoader();
return $loader
->setAncestorClass('PhutilDaemon')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
}
protected final function getPIDDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.pid-directory');
return $this->getControlDirectory($path);
}
protected final function getLogDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.log-directory');
return $this->getControlDirectory($path);
}
private function getControlDirectory($path) {
if (!Filesystem::pathExists($path)) {
list($err) = exec_manual('mkdir -p %s', $path);
if ($err) {
throw new Exception(
"phd requires the directory '{$path}' to exist, but it does not ".
"exist and could not be created. Create this directory or update ".
"'phd.pid-directory' / 'phd.log-directory' in your configuration ".
"to point to an existing directory.");
}
}
return $path;
}
protected final function loadRunningDaemons() {
$daemons = array();
$pid_dir = $this->getPIDDirectory();
$pid_files = Filesystem::listDirectory($pid_dir);
foreach ($pid_files as $pid_file) {
$daemons[] = PhabricatorDaemonReference::newFromFile(
$pid_dir.'/'.$pid_file);
}
return $daemons;
}
protected final function loadAllRunningDaemons() {
$local_daemons = $this->loadRunningDaemons();
$local_ids = array();
foreach ($local_daemons as $daemon) {
$daemon_log = $daemon->getDaemonLog();
if ($daemon_log) {
$local_ids[] = $daemon_log->getID();
}
}
$remote_daemons = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withoutIDs($local_ids)
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->execute();
return array_merge($local_daemons, $remote_daemons);
}
private function findDaemonClass($substring) {
$symbols = $this->loadAvailableDaemonClasses();
$symbols = ipull($symbols, 'name');
$match = array();
foreach ($symbols as $symbol) {
if (stripos($symbol, $substring) !== false) {
if (strtolower($symbol) == strtolower($substring)) {
$match = array($symbol);
break;
} else {
$match[] = $symbol;
}
}
}
if (count($match) == 0) {
throw new PhutilArgumentUsageException(
pht(
"No daemons match '%s'! Use 'phd list' for a list of available ".
"daemons.",
$substring));
} else if (count($match) > 1) {
throw new PhutilArgumentUsageException(
pht(
"Specify a daemon unambiguously. Multiple daemons match '%s': %s.",
$substring,
implode(', ', $match)));
}
return head($match);
}
protected final function launchDaemon($class, array $argv, $debug) {
$daemon = $this->findDaemonClass($class);
$console = PhutilConsole::getConsole();
if ($debug) {
if ($argv) {
$console->writeOut(
pht(
"Launching daemon \"%s\" in debug mode (not daemonized) ".
"with arguments %s.\n",
$daemon,
csprintf('%LR', $argv)));
} else {
$console->writeOut(
pht(
"Launching daemon \"%s\" in debug mode (not daemonized).\n",
$daemon));
}
} else {
if ($argv) {
$console->writeOut(
pht(
"Launching daemon \"%s\" with arguments %s.\n",
$daemon,
csprintf('%LR', $argv)));
} else {
$console->writeOut(
pht(
"Launching daemon \"%s\".\n",
$daemon));
}
}
foreach ($argv as $key => $arg) {
$argv[$key] = escapeshellarg($arg);
}
$flags = array();
if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) {
$flags[] = '--trace';
}
if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) {
$flags[] = '--verbose';
}
if (!$debug) {
$flags[] = '--daemonize';
}
if (!$debug) {
$log_file = $this->getLogDirectory().'/daemons.log';
$flags[] = csprintf('--log=%s', $log_file);
}
$pid_dir = $this->getPIDDirectory();
// TODO: This should be a much better user experience.
Filesystem::assertExists($pid_dir);
Filesystem::assertIsDirectory($pid_dir);
Filesystem::assertWritable($pid_dir);
$flags[] = csprintf('--phd=%s', $pid_dir);
$command = csprintf(
'./phd-daemon %s %C %C',
$daemon,
implode(' ', $flags),
implode(' ', $argv));
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
$daemon_script_dir = $phabricator_root.'/scripts/daemon/';
if ($debug) {
// Don't terminate when the user sends ^C; it will be sent to the
// subprocess which will terminate normally.
pcntl_signal(
SIGINT,
array(__CLASS__, 'ignoreSignal'));
echo "\n phabricator/scripts/daemon/ \$ {$command}\n\n";
phutil_passthru('(cd %s && exec %C)', $daemon_script_dir, $command);
} else {
$future = new ExecFuture('exec %C', $command);
// Play games to keep 'ps' looking reasonable.
$future->setCWD($daemon_script_dir);
$future->resolvex();
}
}
public static function ignoreSignal($signo) {
return;
}
public static function requireExtensions() {
self::mustHaveExtension('pcntl');
self::mustHaveExtension('posix');
}
private static function mustHaveExtension($ext) {
if (!extension_loaded($ext)) {
echo "ERROR: The PHP extension '{$ext}' is not installed. You must ".
"install it to run daemons on this machine.\n";
exit(1);
}
$extension = new ReflectionExtension($ext);
foreach ($extension->getFunctions() as $function) {
$function = $function->name;
if (!function_exists($function)) {
echo "ERROR: The PHP function {$function}() is disabled. You must ".
"enable it to run daemons on this machine.\n";
exit(1);
}
}
}
protected final function willLaunchDaemons() {
$console = PhutilConsole::getConsole();
$console->writeErr(pht('Preparing to launch daemons.')."\n");
$log_dir = $this->getLogDirectory().'/daemons.log';
$console->writeErr(pht("NOTE: Logs will appear in '%s'.", $log_dir)."\n\n");
}
/* -( Commands )----------------------------------------------------------- */
protected final function executeStartCommand($keep_leases = false) {
$console = PhutilConsole::getConsole();
$running = $this->loadRunningDaemons();
// This may include daemons which were launched but which are no longer
// running; check that we actually have active daemons before failing.
foreach ($running as $daemon) {
if ($daemon->isRunning()) {
$message = pht(
"phd start: Unable to start daemons because daemons are already ".
"running.\n".
"You can view running daemons with 'phd status'.\n".
"You can stop running daemons with 'phd stop'.\n".
"You can use 'phd restart' to stop all daemons before starting new ".
"daemons.");
$console->writeErr("%s\n", $message);
exit(1);
}
}
if ($keep_leases) {
$console->writeErr("%s\n", pht('Not touching active task queue leases.'));
} else {
$console->writeErr("%s\n", pht('Freeing active task leases...'));
$count = $this->freeActiveLeases();
$console->writeErr(
"%s\n",
pht('Freed %s task lease(s).', new PhutilNumber($count)));
}
$daemons = array(
array('PhabricatorRepositoryPullLocalDaemon', array()),
array('PhabricatorGarbageCollectorDaemon', array()),
);
$taskmasters = PhabricatorEnv::getEnvConfig('phd.start-taskmasters');
for ($ii = 0; $ii < $taskmasters; $ii++) {
$daemons[] = array('PhabricatorTaskmasterDaemon', array());
}
$this->willLaunchDaemons();
foreach ($daemons as $spec) {
list($name, $argv) = $spec;
$this->launchDaemon($name, $argv, $is_debug = false);
}
$console->writeErr(pht('Done.')."\n");
return 0;
}
- protected final function executeStopCommand(array $pids) {
+ protected final function executeStopCommand(array $pids, $grace_period) {
$console = PhutilConsole::getConsole();
$daemons = $this->loadRunningDaemons();
if (!$daemons) {
$console->writeErr(pht('There are no running Phabricator daemons.')."\n");
return 0;
}
$daemons = mpull($daemons, null, 'getPID');
$running = array();
if (!$pids) {
$running = $daemons;
} else {
// We were given a PID or set of PIDs to kill.
foreach ($pids as $key => $pid) {
if (!preg_match('/^\d+$/', $pid)) {
$console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n");
continue;
} else if (empty($daemons[$pid])) {
$console->writeErr(
pht(
"PID '%s' is not a Phabricator daemon PID. It will not ".
"be killed.",
$pid)."\n");
continue;
} else {
$running[] = $daemons[$pid];
}
}
}
if (empty($running)) {
$console->writeErr(pht('No daemons to kill.')."\n");
return 0;
}
$all_daemons = $running;
- foreach ($running as $key => $daemon) {
- $pid = $daemon->getPID();
- $name = $daemon->getName();
- $console->writeErr(pht("Stopping daemon '%s' (%s)...", $name, $pid)."\n");
- if (!$daemon->isRunning()) {
- $console->writeErr(pht('Daemon is not running.')."\n");
- unset($running[$key]);
- $daemon->updateStatus(PhabricatorDaemonLog::STATUS_EXITED);
- } else {
- posix_kill($pid, SIGINT);
- }
+ // If we're doing a graceful shutdown, try SIGINT first.
+ if ($grace_period) {
+ $running = $this->sendSignal($running, SIGINT, $grace_period);
}
- $start = time();
- do {
- foreach ($running as $key => $daemon) {
- $pid = $daemon->getPID();
- if (!$daemon->isRunning()) {
- $console->writeOut(pht('Daemon %s exited normally.', $pid)."\n");
- unset($running[$key]);
- }
- }
- if (empty($running)) {
- break;
- }
- usleep(100000);
- } while (time() < $start + 15);
+ // If we still have daemons, SIGTERM them.
+ if ($running) {
+ $running = $this->sendSignal($running, SIGTERM, 15);
+ }
- foreach ($running as $key => $daemon) {
- $pid = $daemon->getPID();
- $console->writeErr(pht('Sending daemon %s a SIGKILL.', $pid)."\n");
- posix_kill($pid, SIGKILL);
+ // If the overseer is still alive, SIGKILL it.
+ if ($running) {
+ $this->sendSignal($running, SIGKILL, 0);
}
foreach ($all_daemons as $daemon) {
if ($daemon->getPIDFile()) {
Filesystem::remove($daemon->getPIDFile());
}
}
return 0;
}
+ private function sendSignal(array $daemons, $signo, $wait) {
+ $console = PhutilConsole::getConsole();
+
+ foreach ($daemons as $key => $daemon) {
+ $pid = $daemon->getPID();
+ $name = $daemon->getName();
+
+ switch ($signo) {
+ case SIGINT:
+ $message = pht("Interrupting daemon '%s' (%s)...", $name, $pid);
+ break;
+ case SIGTERM:
+ $message = pht("Terminating daemon '%s' (%s)...", $name, $pid);
+ break;
+ case SIGKILL:
+ $message = pht("Killing daemon '%s' (%s)...", $name, $pid);
+ break;
+ }
+
+ $console->writeOut("%s\n", $message);
+ posix_kill($pid, $signo);
+ }
+
+ if ($wait) {
+ $start = PhabricatorTime::getNow();
+ do {
+ foreach ($daemons as $key => $daemon) {
+ $pid = $daemon->getPID();
+ if (!$daemon->isRunning()) {
+ $console->writeOut(pht('Daemon %s exited.', $pid)."\n");
+ unset($daemons[$key]);
+ }
+ }
+ if (empty($daemons)) {
+ break;
+ }
+ usleep(100000);
+ } while (PhabricatorTime::getNow() < $start + $wait);
+ }
+
+ return $daemons;
+ }
+
private function freeActiveLeases() {
$task_table = id(new PhabricatorWorkerActiveTask());
$conn_w = $task_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP()
WHERE leaseExpires > UNIX_TIMESTAMP()',
$task_table->getTableName());
return $conn_w->getAffectedRows();
}
}
diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
index 8302b2e826..6da5155193 100644
--- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
+++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php
@@ -1,173 +1,176 @@
<?php
final class PhabricatorDaemonLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const STATUS_ALL = 'status-all';
const STATUS_ALIVE = 'status-alive';
private $ids;
private $notIDs;
private $status = self::STATUS_ALL;
private $daemonClasses;
private $allowStatusWrites;
public static function getTimeUntilUnknown() {
return 3 * PhutilDaemonOverseer::HEARTBEAT_WAIT;
}
public static function getTimeUntilDead() {
return 30 * PhutilDaemonOverseer::HEARTBEAT_WAIT;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withoutIDs(array $ids) {
$this->notIDs = $ids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withDaemonClasses(array $classes) {
$this->daemonClasses = $classes;
return $this;
}
public function setAllowStatusWrites($allow) {
$this->allowStatusWrites = $allow;
return $this;
}
public function loadPage() {
$table = new PhabricatorDaemonLog();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
public function willFilterPage(array $daemons) {
$unknown_delay = PhabricatorDaemonLogQuery::getTimeUntilUnknown();
$dead_delay = PhabricatorDaemonLogQuery::getTimeUntilDead();
$status_running = PhabricatorDaemonLog::STATUS_RUNNING;
$status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN;
$status_wait = PhabricatorDaemonLog::STATUS_WAIT;
+ $status_exiting = PhabricatorDaemonLog::STATUS_EXITING;
$status_exited = PhabricatorDaemonLog::STATUS_EXITED;
$status_dead = PhabricatorDaemonLog::STATUS_DEAD;
$filter = array_fuse($this->getStatusConstants());
foreach ($daemons as $key => $daemon) {
$status = $daemon->getStatus();
$seen = $daemon->getDateModified();
$is_running = ($status == $status_running) ||
- ($status == $status_wait);
+ ($status == $status_wait) ||
+ ($status == $status_exiting);
// If we haven't seen the daemon recently, downgrade its status to
// unknown.
$unknown_time = ($seen + $unknown_delay);
if ($is_running && ($unknown_time < time())) {
$status = $status_unknown;
}
// If the daemon hasn't been seen in quite a while, assume it is dead.
$dead_time = ($seen + $dead_delay);
if (($status == $status_unknown) && ($dead_time < time())) {
$status = $status_dead;
}
// If we changed the daemon's status, adjust it.
if ($status != $daemon->getStatus()) {
$daemon->setStatus($status);
// ...and write it, if we're in a context where that's reasonable.
if ($this->allowStatusWrites) {
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
$daemon->save();
unset($guard);
}
}
// If the daemon no longer matches the filter, get rid of it.
if ($filter) {
if (empty($filter[$daemon->getStatus()])) {
unset($daemons[$key]);
}
}
}
return $daemons;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->notIDs) {
$where[] = qsprintf(
$conn_r,
'id NOT IN (%Ld)',
$this->notIDs);
}
if ($this->getStatusConstants()) {
$where[] = qsprintf(
$conn_r,
'status IN (%Ls)',
$this->getStatusConstants());
}
if ($this->daemonClasses) {
$where[] = qsprintf(
$conn_r,
'daemon IN (%Ls)',
$this->daemonClasses);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function getStatusConstants() {
$status = $this->status;
switch ($status) {
case self::STATUS_ALL:
return array();
case self::STATUS_ALIVE:
return array(
PhabricatorDaemonLog::STATUS_UNKNOWN,
PhabricatorDaemonLog::STATUS_RUNNING,
PhabricatorDaemonLog::STATUS_WAIT,
+ PhabricatorDaemonLog::STATUS_EXITING,
);
default:
throw new Exception("Unknown status '{$status}'!");
}
}
public function getQueryApplicationClass() {
return 'PhabricatorDaemonsApplication';
}
}
diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php
index d8f264f707..73f3a78eb7 100644
--- a/src/applications/daemon/storage/PhabricatorDaemonLog.php
+++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php
@@ -1,61 +1,62 @@
<?php
final class PhabricatorDaemonLog extends PhabricatorDaemonDAO
implements PhabricatorPolicyInterface {
const STATUS_UNKNOWN = 'unknown';
const STATUS_RUNNING = 'run';
const STATUS_DEAD = 'dead';
const STATUS_WAIT = 'wait';
+ const STATUS_EXITING = 'exiting';
const STATUS_EXITED = 'exit';
protected $daemon;
protected $host;
protected $pid;
protected $argv;
protected $explicitArgv = array();
protected $status;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'argv' => self::SERIALIZATION_JSON,
'explicitArgv' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function getExplicitArgv() {
$argv = $this->explicitArgv;
if (!is_array($argv)) {
return array();
}
return $argv;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return null;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_ADMIN;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
index 70bf095c43..e9f7876d07 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
@@ -1,76 +1,79 @@
<?php
final class PhabricatorDaemonLogListView extends AphrontView {
private $daemonLogs;
public function setDaemonLogs(array $daemon_logs) {
assert_instances_of($daemon_logs, 'PhabricatorDaemonLog');
$this->daemonLogs = $daemon_logs;
return $this;
}
public function render() {
$rows = array();
if (!$this->user) {
throw new Exception('Call setUser() before rendering!');
}
- $list = id(new PHUIObjectItemListView())
- ->setFlush(true);
+ $list = new PHUIObjectItemListView();
foreach ($this->daemonLogs as $log) {
$id = $log->getID();
$epoch = $log->getDateCreated();
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Daemon %s', $id))
->setHeader($log->getDaemon())
->setHref("/daemon/log/{$id}/")
->addIcon('none', phabricator_datetime($epoch, $this->user));
$status = $log->getStatus();
switch ($status) {
case PhabricatorDaemonLog::STATUS_RUNNING:
$item->setBarColor('green');
$item->addAttribute(pht('This daemon is running.'));
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$item->setBarColor('red');
$item->addAttribute(
pht(
'This daemon is lost or exited uncleanly, and is presumed '.
'dead.'));
$item->addIcon('fa-times grey', pht('Dead'));
break;
+ case PhabricatorDaemonLog::STATUS_EXITING:
+ $item->addAttribute(pht('This daemon is exiting.'));
+ $item->addIcon('fa-check', pht('Exiting'));
+ break;
case PhabricatorDaemonLog::STATUS_EXITED:
$item->setDisabled(true);
$item->addAttribute(pht('This daemon exited cleanly.'));
$item->addIcon('fa-check grey', pht('Exited'));
break;
case PhabricatorDaemonLog::STATUS_WAIT:
$item->setBarColor('blue');
$item->addAttribute(
pht(
'This daemon encountered an error recently and is waiting a '.
'moment to restart.'));
$item->addIcon('fa-clock-o grey', pht('Waiting'));
break;
case PhabricatorDaemonLog::STATUS_UNKNOWN:
default:
$item->setBarColor('orange');
$item->addAttribute(
pht(
'This daemon has not reported its status recently. It may '.
'have exited uncleanly.'));
$item->addIcon('fa-exclamation-circle', pht('Unknown'));
break;
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php
index f4f680b23a..4cce5504f2 100644
--- a/src/applications/fact/daemon/PhabricatorFactDaemon.php
+++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php
@@ -1,205 +1,205 @@
<?php
final class PhabricatorFactDaemon extends PhabricatorDaemon {
private $engines;
const RAW_FACT_BUFFER_LIMIT = 128;
public function run() {
$this->setEngines(PhabricatorFactEngine::loadAllEngines());
- while (true) {
+ while (!$this->shouldExit()) {
$iterators = $this->getAllApplicationIterators();
foreach ($iterators as $iterator_name => $iterator) {
$this->processIteratorWithCursor($iterator_name, $iterator);
}
$this->processAggregates();
$this->log('Zzz...');
$this->sleep(60 * 5);
}
}
public static function getAllApplicationIterators() {
$apps = PhabricatorApplication::getAllInstalledApplications();
$iterators = array();
foreach ($apps as $app) {
foreach ($app->getFactObjectsForAnalysis() as $object) {
$iterator = new PhabricatorFactUpdateIterator($object);
$iterators[get_class($object)] = $iterator;
}
}
return $iterators;
}
public function processIteratorWithCursor($iterator_name, $iterator) {
$this->log("Processing cursor '{$iterator_name}'.");
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$iterator_name);
if (!$cursor) {
$cursor = new PhabricatorFactCursor();
$cursor->setName($iterator_name);
$position = null;
} else {
$position = $cursor->getPosition();
}
if ($position) {
$iterator->setPosition($position);
}
$new_cursor_position = $this->processIterator($iterator);
if ($new_cursor_position) {
$cursor->setPosition($new_cursor_position);
$cursor->save();
}
}
public function setEngines(array $engines) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$this->engines = $engines;
return $this;
}
public function processIterator($iterator) {
$result = null;
$raw_facts = array();
foreach ($iterator as $key => $object) {
$phid = $object->getPHID();
$this->log("Processing {$phid}...");
$raw_facts[$phid] = $this->computeRawFacts($object);
if (count($raw_facts) > self::RAW_FACT_BUFFER_LIMIT) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
$result = $key;
}
if ($raw_facts) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
return $result;
}
public function processAggregates() {
$this->log('Processing aggregates.');
$facts = $this->computeAggregateFacts();
$this->updateAggregateFacts($facts);
}
private function computeAggregateFacts() {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeAggregateFacts()) {
continue;
}
$facts[] = $engine->computeAggregateFacts();
}
return array_mergev($facts);
}
private function computeRawFacts(PhabricatorLiskDAO $object) {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeRawFactsForObject($object)) {
continue;
}
$facts[] = $engine->computeRawFactsForObject($object);
}
return array_mergev($facts);
}
private function updateRawFacts(array $map) {
foreach ($map as $phid => $facts) {
assert_instances_of($facts, 'PhabricatorFactRaw');
}
$phids = array_keys($map);
if (!$phids) {
return;
}
$table = new PhabricatorFactRaw();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d, %d, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getObjectA(),
$fact->getValueX(),
$fact->getValueY(),
$fact->getEpoch());
}
}
$table->openTransaction();
queryfx(
$conn,
'DELETE FROM %T WHERE objectPHID IN (%Ls)',
$table_name,
$phids);
if ($sql) {
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T
(factType, objectPHID, objectA, valueX, valueY, epoch)
VALUES %Q',
$table_name,
implode(', ', $chunk));
}
}
$table->saveTransaction();
}
private function updateAggregateFacts(array $facts) {
if (!$facts) {
return;
}
$table = new PhabricatorFactAggregate();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getValueX());
}
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (factType, objectPHID, valueX) VALUES %Q
ON DUPLICATE KEY UPDATE valueX = VALUES(valueX)',
$table_name,
implode(', ', $chunk));
}
}
}
diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
index 673ae4b8eb..ceadbe60bf 100644
--- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
+++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
@@ -1,432 +1,438 @@
<?php
/**
* Run pull commands on local working copies to keep them up to date. This
* daemon handles all repository types.
*
* By default, the daemon pulls **every** repository. If you want it to be
* responsible for only some repositories, you can launch it with a list of
* PHIDs or callsigns:
*
* ./phd launch repositorypulllocal -- X Q Z
*
* You can also launch a daemon which is responsible for all //but// one or
* more repositories:
*
* ./phd launch repositorypulllocal -- --not A --not B
*
* If you have a very large number of repositories and some aren't being pulled
* as frequently as you'd like, you can either change the pull frequency of
* the less-important repositories to a larger number (so the daemon will skip
* them more often) or launch one daemon for all the less-important repositories
* and one for the more important repositories (or one for each more important
* repository).
*
* @task pull Pulling Repositories
*/
final class PhabricatorRepositoryPullLocalDaemon
extends PhabricatorDaemon {
/* -( Pulling Repositories )----------------------------------------------- */
/**
* @task pull
*/
public function run() {
$argv = $this->getArgv();
array_unshift($argv, __CLASS__);
$args = new PhutilArgumentParser($argv);
$args->parse(
array(
array(
'name' => 'no-discovery',
'help' => 'Pull only, without discovering commits.',
),
array(
'name' => 'not',
'param' => 'repository',
'repeat' => true,
'help' => 'Do not pull __repository__.',
),
array(
'name' => 'repositories',
'wildcard' => true,
'help' => 'Pull specific __repositories__ instead of all.',
),
));
$no_discovery = $args->getArg('no-discovery');
$include = $args->getArg('repositories');
$exclude = $args->getArg('not');
// Each repository has an individual pull frequency; after we pull it,
// wait that long to pull it again. When we start up, try to pull everything
// serially.
$retry_after = array();
$min_sleep = 15;
$max_futures = 4;
$futures = array();
$queue = array();
- while (true) {
+ while (!$this->shouldExit()) {
$pullable = $this->loadPullableRepositories($include, $exclude);
// If any repositories have the NEEDS_UPDATE flag set, pull them
// as soon as possible.
$need_update_messages = $this->loadRepositoryUpdateMessages();
foreach ($need_update_messages as $message) {
$repo = idx($pullable, $message->getRepositoryID());
if (!$repo) {
continue;
}
$this->log(
pht(
'Got an update message for repository "%s"!',
$repo->getMonogram()));
$retry_after[$message->getRepositoryID()] = time();
}
// If any repositories were deleted, remove them from the retry timer map
// so we don't end up with a retry timer that never gets updated and
// causes us to sleep for the minimum amount of time.
$retry_after = array_select_keys(
$retry_after,
array_keys($pullable));
// Figure out which repositories we need to queue for an update.
foreach ($pullable as $id => $repository) {
$monogram = $repository->getMonogram();
if (isset($futures[$id])) {
$this->log(pht('Repository "%s" is currently updating.', $monogram));
continue;
}
if (isset($queue[$id])) {
$this->log(pht('Repository "%s" is already queued.', $monogram));
continue;
}
$after = idx($retry_after, $id, 0);
if ($after > time()) {
$this->log(
pht(
'Repository "%s" is not due for an update for %s second(s).',
$monogram,
new PhutilNumber($after - time())));
continue;
}
if (!$after) {
$this->log(
pht(
'Scheduling repository "%s" for an initial update.',
$monogram));
} else {
$this->log(
pht(
'Scheduling repository "%s" for an update (%s seconds overdue).',
$monogram,
new PhutilNumber(time() - $after)));
}
$queue[$id] = $after;
}
// Process repositories in the order they became candidates for updates.
asort($queue);
// Dequeue repositories until we hit maximum parallelism.
while ($queue && (count($futures) < $max_futures)) {
foreach ($queue as $id => $time) {
$repository = idx($pullable, $id);
if (!$repository) {
$this->log(
pht('Repository %s is no longer pullable; skipping.', $id));
break;
}
$monogram = $repository->getMonogram();
$this->log(pht('Starting update for repository "%s".', $monogram));
unset($queue[$id]);
$futures[$id] = $this->buildUpdateFuture(
$repository,
$no_discovery);
break;
}
}
if ($queue) {
$this->log(
pht(
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.',
new PhutilNumber(count($queue))));
}
if ($futures) {
$iterator = id(new FutureIterator($futures))
->setUpdateInterval($min_sleep);
foreach ($iterator as $id => $future) {
$this->stillWorking();
if ($future === null) {
$this->log(pht('Waiting for updates to complete...'));
$this->stillWorking();
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Interrupted by pending updates!'));
break;
}
continue;
}
unset($futures[$id]);
$retry_after[$id] = $this->resolveUpdateFuture(
$pullable[$id],
$future,
$min_sleep);
// We have a free slot now, so go try to fill it.
break;
}
// Jump back into prioritization if we had any futures to deal with.
continue;
}
$this->waitForUpdates($min_sleep, $retry_after);
}
}
/**
* @task pull
*/
private function buildUpdateFuture(
PhabricatorRepository $repository,
$no_discovery) {
$bin = dirname(phutil_get_library_root('phabricator')).'/bin/repository';
$flags = array();
if ($no_discovery) {
$flags[] = '--no-discovery';
}
$callsign = $repository->getCallsign();
$future = new ExecFuture('%s update %Ls -- %s', $bin, $flags, $callsign);
// Sometimes, the underlying VCS commands will hang indefinitely. We've
// observed this occasionally with GitHub, and other users have observed
// it with other VCS servers.
// To limit the damage this can cause, kill the update out after a
// reasonable amount of time, under the assumption that it has hung.
// Since it's hard to know what a "reasonable" amount of time is given that
// users may be downloading a repository full of pirated movies over a
// potato, these limits are fairly generous. Repositories exceeding these
// limits can be manually pulled with `bin/repository update X`, which can
// just run for as long as it wants.
if ($repository->isImporting()) {
$timeout = phutil_units('4 hours in seconds');
} else {
$timeout = phutil_units('15 minutes in seconds');
}
$future->setTimeout($timeout);
return $future;
}
/**
* @task pull
*/
private function loadRepositoryUpdateMessages() {
$type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE;
return id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('statusType = %s', $type_need_update);
}
/**
* @task pull
*/
private function loadPullableRepositories(array $include, array $exclude) {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer());
if ($include) {
$query->withCallsigns($include);
}
$repositories = $query->execute();
if ($include) {
$by_callsign = mpull($repositories, null, 'getCallsign');
foreach ($include as $name) {
if (empty($by_callsign[$name])) {
throw new Exception(
"No repository exists with callsign '{$name}'!");
}
}
}
if ($exclude) {
$exclude = array_fuse($exclude);
foreach ($repositories as $key => $repository) {
if (isset($exclude[$repository->getCallsign()])) {
unset($repositories[$key]);
}
}
}
foreach ($repositories as $key => $repository) {
if (!$repository->isTracked()) {
unset($repositories[$key]);
}
}
// Shuffle the repositories, then re-key the array since shuffle()
// discards keys. This is mostly for startup, we'll use soft priorities
// later.
shuffle($repositories);
$repositories = mpull($repositories, null, 'getID');
return $repositories;
}
/**
* @task pull
*/
private function resolveUpdateFuture(
PhabricatorRepository $repository,
ExecFuture $future,
$min_sleep) {
$monogram = $repository->getMonogram();
$this->log(pht('Resolving update for "%s".', $monogram));
try {
list($stdout, $stderr) = $future->resolvex();
} catch (Exception $ex) {
$proxy = new PhutilProxyException(
pht(
'Error while updating the "%s" repository.',
$repository->getMonogram()),
$ex);
phlog($proxy);
return time() + $min_sleep;
}
if (strlen($stderr)) {
$stderr_msg = pht(
'Unexpected output while updating repository "%s": %s',
$monogram,
$stderr);
phlog($stderr_msg);
}
$sleep_for = (int)$repository->getDetail('pull-frequency', $min_sleep);
// Smart wait: pull rarely used repositories less frequently. Find the
// most recent commit which is older than the current time (this keeps us
// from spinning on repositories with a silly commit post-dated to some time
// in 2037), and adjust how frequently we pull based on how frequently this
// repository updates.
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('w'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$repository->getID(),
time() + $min_sleep);
if ($last_commit) {
$time_since_commit = (time() + $min_sleep) - $last_commit['epoch'];
// Wait 0.5% of the time since the last commit before we pull. This gives
// us these wait times:
//
// 50 minutes or less: 15 seconds
// about 3 hours: 1 minute
// about 16 hours: 5 minutes
// about 2 days: 15 minutes
// 50 days or more: 6 hours
$smart_wait = ($time_since_commit / 200);
$smart_wait = min($smart_wait, phutil_units('6 hours in seconds'));
$this->log(
pht(
'Last commit to repository "%s" was %s seconds ago; considering '.
'a wait of %s seconds before update.',
$repository->getMonogram(),
new PhutilNumber($time_since_commit),
new PhutilNumber($smart_wait)));
$smart_wait = max(15, $smart_wait);
$sleep_for = max($smart_wait, $sleep_for);
}
if ($sleep_for < $min_sleep) {
$sleep_for = $min_sleep;
}
return time() + $sleep_for;
}
/**
* Sleep for a short period of time, waiting for update messages from the
*
*
* @task pull
*/
private function waitForUpdates($min_sleep, array $retry_after) {
$this->log(
pht('No repositories need updates right now, sleeping...'));
$sleep_until = time() + $min_sleep;
if ($retry_after) {
$sleep_until = min($sleep_until, min($retry_after));
}
while (($sleep_until - time()) > 0) {
$sleep_duration = ($sleep_until - time());
$this->log(
pht(
'Sleeping for %s more second(s)...',
new PhutilNumber($sleep_duration)));
$this->sleep(1);
+
+ if ($this->shouldExit()) {
+ $this->log(pht('Awakened from sleep by graceful shutdown!'));
+ return;
+ }
+
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Awakened from sleep by pending updates!'));
break;
}
}
}
}
diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
index 0d6675849d..bdc2092eaf 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -1,148 +1,148 @@
<?php
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*/
final class PhabricatorBot extends PhabricatorDaemon {
private $handlers;
private $conduit;
private $config;
private $pollFrequency;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception('usage: PhabricatorBot <json_config_file>');
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$nick = idx($config, 'nick', 'phabot');
$handlers = idx($config, 'handlers', array());
$protocol_adapter_class = idx(
$config,
'protocol-adapter',
'PhabricatorIRCProtocolAdapter');
$this->pollFrequency = idx($config, 'poll-frequency', 1);
$this->config = $config;
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$ca_bundle = idx($config, 'https.cabundle');
if ($ca_bundle) {
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_host = (string)$conduit_uri->setPath('/');
$conduit_uri = (string)$conduit_uri->setPath('/api/');
$conduit = new ConduitClient($conduit_uri);
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'PhabricatorBot',
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'host' => $conduit_host,
'user' => $conduit_user,
'certificate' => $conduit_cert,
));
$this->conduit = $conduit;
}
// Instantiate Protocol Adapter, for now follow same technique as
// handler instantiation
$this->protocolAdapter = newv($protocol_adapter_class, array());
$this->protocolAdapter
->setConfig($this->config)
->connect();
$this->runLoop();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runLoop() {
do {
$this->stillWorking();
$messages = $this->protocolAdapter->getNextMessages($this->pollFrequency);
if (count($messages) > 0) {
foreach ($messages as $message) {
$this->routeMessage($message);
}
}
foreach ($this->handlers as $handler) {
$handler->runBackgroundTasks();
}
- } while (true);
+ } while (!$this->shouldExit());
}
public function writeMessage(PhabricatorBotMessage $message) {
return $this->protocolAdapter->writeMessage($message);
}
private function routeMessage(PhabricatorBotMessage $message) {
$ignore = $this->getConfig('ignore');
if ($ignore) {
$sender = $message->getSender();
if ($sender && in_array($sender->getName(), $ignore)) {
return;
}
}
if ($message->getCommand() == 'LOG') {
$this->log('[LOG] '.$message->getBody());
}
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function getAdapter() {
return $this->protocolAdapter;
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
"This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
"'conduit.user' and 'conduit.cert' in the configuration to connect.");
}
return $this->conduit;
}
}
diff --git a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php
index 5982a8de8a..5dd1f69ac3 100644
--- a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php
+++ b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php
@@ -1,38 +1,38 @@
<?php
/**
* Collects old logs and caches to reduce the amount of data stored in the
* database.
*/
final class PhabricatorGarbageCollectorDaemon extends PhabricatorDaemon {
public function run() {
$collectors = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorGarbageCollector')
->loadObjects();
do {
foreach ($collectors as $name => $collector) {
$more_garbage = false;
do {
if ($more_garbage) {
$this->log(pht('Collecting more garbage with "%s".', $name));
} else {
$this->log(pht('Collecting garbage with "%s".', $name));
}
$more_garbage = $collector->collectGarbage();
$this->stillWorking();
} while ($more_garbage);
}
// We made it to the end of the run cycle of every GC, so we're more or
// less caught up. Ease off the GC loop so we don't keep doing table
// scans just to delete a handful of rows; wake up in a few hours.
$this->log(pht('All caught up, waiting for more garbage.'));
$this->sleep(4 * (60 * 60));
- } while (true);
+ } while (!$this->shouldExit());
}
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
index 879aa76588..9b9e3a400a 100644
--- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
+++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
@@ -1,46 +1,46 @@
<?php
final class PhabricatorTaskmasterDaemon extends PhabricatorDaemon {
public function run() {
$sleep = 0;
do {
$tasks = id(new PhabricatorWorkerLeaseQuery())
->setLimit(1)
->execute();
if ($tasks) {
foreach ($tasks as $task) {
$id = $task->getID();
$class = $task->getTaskClass();
$this->log("Working on task {$id} ({$class})...");
$task = $task->executeTask();
$ex = $task->getExecutionException();
if ($ex) {
if ($ex instanceof PhabricatorWorkerPermanentFailureException) {
$this->log("Task {$id} failed permanently.");
} else if ($ex instanceof PhabricatorWorkerYieldException) {
$this->log(pht('Task %s yielded.', $id));
} else {
$this->log("Task {$id} failed!");
throw new PhutilProxyException(
"Error while executing task ID {$id} from queue.",
$ex);
}
} else {
$this->log("Task {$id} complete! Moved to archive.");
}
}
$sleep = 0;
} else {
$sleep = min($sleep + 1, 30);
}
$this->sleep($sleep);
- } while (true);
+ } while (!$this->shouldExit());
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 6:51 PM (3 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
166202
Default Alt Text
(62 KB)

Event Timeline