diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -54,6 +54,10 @@ $tag->setName(pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: + $tag->setBackgroundColor(PHUITagView::COLOR_YELLOW); + $tag->setName(pht('Exiting')); + break; + case PhabricatorDaemonLog::STATUS_EXITED: $tag->setBackgroundColor(PHUITagView::COLOR_GREY); $tag->setName(pht('Exited')); break; @@ -138,6 +142,10 @@ break; case PhabricatorDaemonLog::STATUS_EXITED: $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; } diff --git a/src/applications/daemon/event/PhabricatorDaemonEventListener.php b/src/applications/daemon/event/PhabricatorDaemonEventListener.php --- a/src/applications/daemon/event/PhabricatorDaemonEventListener.php +++ b/src/applications/daemon/event/PhabricatorDaemonEventListener.php @@ -8,6 +8,7 @@ $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); } @@ -22,6 +23,9 @@ 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; @@ -86,6 +90,13 @@ } } + 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'); diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php --- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php @@ -9,11 +9,22 @@ ->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; } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php --- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php @@ -13,6 +13,14 @@ ->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, ), @@ -21,7 +29,8 @@ 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 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -286,7 +286,7 @@ return 0; } - protected final function executeStopCommand(array $pids) { + protected final function executeStopCommand(array $pids, $grace_period) { $console = PhutilConsole::getConsole(); $daemons = $this->loadRunningDaemons(); @@ -325,39 +325,20 @@ } $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) { @@ -369,6 +350,49 @@ 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'); diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php --- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php +++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php @@ -67,6 +67,7 @@ $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; @@ -77,7 +78,8 @@ $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. @@ -160,6 +162,7 @@ PhabricatorDaemonLog::STATUS_UNKNOWN, PhabricatorDaemonLog::STATUS_RUNNING, PhabricatorDaemonLog::STATUS_WAIT, + PhabricatorDaemonLog::STATUS_EXITING, ); default: throw new Exception("Unknown status '{$status}'!"); diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -7,6 +7,7 @@ const STATUS_RUNNING = 'run'; const STATUS_DEAD = 'dead'; const STATUS_WAIT = 'wait'; + const STATUS_EXITING = 'exiting'; const STATUS_EXITED = 'exit'; protected $daemon; diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -17,8 +17,7 @@ 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(); @@ -43,6 +42,10 @@ '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.')); diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php --- a/src/applications/fact/daemon/PhabricatorFactDaemon.php +++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php @@ -8,7 +8,7 @@ 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); diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -71,7 +71,7 @@ $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 @@ -422,6 +422,12 @@ 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 --- a/src/infrastructure/daemon/bot/PhabricatorBot.php +++ b/src/infrastructure/daemon/bot/PhabricatorBot.php @@ -103,7 +103,7 @@ foreach ($this->handlers as $handler) { $handler->runBackgroundTasks(); } - } while (true); + } while (!$this->shouldExit()); } public function writeMessage(PhabricatorBotMessage $message) { diff --git a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php --- a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php +++ b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php @@ -31,7 +31,7 @@ // 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 --- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php @@ -40,7 +40,7 @@ } $this->sleep($sleep); - } while (true); + } while (!$this->shouldExit()); } }