Path: blob/master/src/infrastructure/daemon/PhutilDaemonHandle.php
12241 views
<?php12final class PhutilDaemonHandle extends Phobject {34const EVENT_DID_LAUNCH = 'daemon.didLaunch';5const EVENT_DID_LOG = 'daemon.didLogMessage';6const EVENT_DID_HEARTBEAT = 'daemon.didHeartbeat';7const EVENT_WILL_GRACEFUL = 'daemon.willGraceful';8const EVENT_WILL_EXIT = 'daemon.willExit';910private $pool;11private $properties;12private $future;13private $argv;1415private $restartAt;16private $busyEpoch;1718private $daemonID;19private $deadline;20private $heartbeat;21private $stdoutBuffer;22private $shouldRestart = true;23private $shouldShutdown;24private $hibernating = false;25private $shouldSendExitEvent = false;2627private function __construct() {28// <empty>29}3031public static function newFromConfig(array $config) {32PhutilTypeSpec::checkMap(33$config,34array(35'class' => 'string',36'argv' => 'optional list<string>',37'load' => 'optional list<string>',38'log' => 'optional string|null',39'down' => 'optional int',40));4142$config = $config + array(43'argv' => array(),44'load' => array(),45'log' => null,46'down' => 15,47);4849$daemon = new self();50$daemon->properties = $config;51$daemon->daemonID = $daemon->generateDaemonID();5253return $daemon;54}5556public function setDaemonPool(PhutilDaemonPool $daemon_pool) {57$this->pool = $daemon_pool;58return $this;59}6061public function getDaemonPool() {62return $this->pool;63}6465public function getBusyEpoch() {66return $this->busyEpoch;67}6869public function getDaemonClass() {70return $this->getProperty('class');71}7273private function getProperty($key) {74return idx($this->properties, $key);75}7677public function setCommandLineArguments(array $arguments) {78$this->argv = $arguments;79return $this;80}8182public function getCommandLineArguments() {83return $this->argv;84}8586public function getDaemonArguments() {87return $this->getProperty('argv');88}8990public function didLaunch() {91$this->restartAt = time();92$this->shouldSendExitEvent = true;9394$this->dispatchEvent(95self::EVENT_DID_LAUNCH,96array(97'argv' => $this->getCommandLineArguments(),98'explicitArgv' => $this->getDaemonArguments(),99));100101return $this;102}103104public function isRunning() {105return (bool)$this->getFuture();106}107108public function isHibernating() {109return110!$this->isRunning() &&111!$this->isDone() &&112$this->hibernating;113}114115public function wakeFromHibernation() {116if (!$this->isHibernating()) {117return $this;118}119120$this->logMessage(121'WAKE',122pht(123'Process is being awakened from hibernation.'));124125$this->restartAt = time();126$this->update();127128return $this;129}130131public function isDone() {132return (!$this->shouldRestart && !$this->isRunning());133}134135public function update() {136if (!$this->isRunning()) {137if (!$this->shouldRestart) {138return;139}140if (!$this->restartAt || (time() < $this->restartAt)) {141return;142}143if ($this->shouldShutdown) {144return;145}146$this->startDaemonProcess();147}148149$future = $this->getFuture();150151$result = null;152$caught = null;153if ($future->canResolve()) {154$this->future = null;155try {156$result = $future->resolve();157} catch (Exception $ex) {158$caught = $ex;159} catch (Throwable $ex) {160$caught = $ex;161}162}163164list($stdout, $stderr) = $future->read();165$future->discardBuffers();166167if (strlen($stdout)) {168$this->didReadStdout($stdout);169}170171$stderr = trim($stderr);172if (strlen($stderr)) {173foreach (phutil_split_lines($stderr, false) as $line) {174$this->logMessage('STDE', $line);175}176}177178if ($result !== null || $caught !== null) {179180if ($caught) {181$message = pht(182'Process failed with exception: %s',183$caught->getMessage());184$this->logMessage('FAIL', $message);185} else {186list($err) = $result;187188if ($err) {189$this->logMessage('FAIL', pht('Process exited with error %s.', $err));190} else {191$this->logMessage('DONE', pht('Process exited normally.'));192}193}194195if ($this->shouldShutdown) {196$this->restartAt = null;197} else {198$this->scheduleRestart();199}200}201202$this->updateHeartbeatEvent();203$this->updateHangDetection();204}205206private function updateHeartbeatEvent() {207if ($this->heartbeat > time()) {208return;209}210211$this->heartbeat = time() + $this->getHeartbeatEventFrequency();212$this->dispatchEvent(self::EVENT_DID_HEARTBEAT);213}214215private function updateHangDetection() {216if (!$this->isRunning()) {217return;218}219220if (time() > $this->deadline) {221$this->logMessage('HANG', pht('Hang detected. Restarting process.'));222$this->annihilateProcessGroup();223$this->scheduleRestart();224}225}226227private function scheduleRestart() {228// Wait a minimum of a few sceconds before restarting, but we may wait229// longer if the daemon has initiated hibernation.230$default_restart = time() + self::getWaitBeforeRestart();231if ($default_restart >= $this->restartAt) {232$this->restartAt = $default_restart;233}234235$this->logMessage(236'WAIT',237pht(238'Waiting %s second(s) to restart process.',239new PhutilNumber($this->restartAt - time())));240}241242/**243* Generate a unique ID for this daemon.244*245* @return string A unique daemon ID.246*/247private function generateDaemonID() {248return substr(getmypid().':'.Filesystem::readRandomCharacters(12), 0, 12);249}250251public function getDaemonID() {252return $this->daemonID;253}254255private function getFuture() {256return $this->future;257}258259private function getPID() {260$future = $this->getFuture();261262if (!$future) {263return null;264}265266if (!$future->hasPID()) {267return null;268}269270return $future->getPID();271}272273private function getCaptureBufferSize() {274return 65535;275}276277private function getRequiredHeartbeatFrequency() {278return 86400;279}280281public static function getWaitBeforeRestart() {282return 5;283}284285public static function getHeartbeatEventFrequency() {286return 120;287}288289private function getKillDelay() {290return 3;291}292293private function getDaemonCWD() {294$root = dirname(phutil_get_library_root('phabricator'));295return $root.'/scripts/daemon/exec/';296}297298private function newExecFuture() {299$class = $this->getDaemonClass();300$argv = $this->getCommandLineArguments();301$buffer_size = $this->getCaptureBufferSize();302303// NOTE: PHP implements proc_open() by running 'sh -c'. On most systems this304// is bash, but on Ubuntu it's dash. When you proc_open() using bash, you305// get one new process (the command you ran). When you proc_open() using306// dash, you get two new processes: the command you ran and a parent307// "dash -c" (or "sh -c") process. This means that the child process's PID308// is actually the 'dash' PID, not the command's PID. To avoid this, use309// 'exec' to replace the shell process with the real process; without this,310// the child will call posix_getppid(), be given the pid of the 'sh -c'311// process, and send it SIGUSR1 to keepalive which will terminate it312// immediately. We also won't be able to do process group management because313// the shell process won't properly posix_setsid() so the pgid of the child314// won't be meaningful.315316$config = $this->properties;317unset($config['class']);318$config = phutil_json_encode($config);319320return id(new ExecFuture('exec ./exec_daemon.php %s %Ls', $class, $argv))321->setCWD($this->getDaemonCWD())322->setStdoutSizeLimit($buffer_size)323->setStderrSizeLimit($buffer_size)324->write($config);325}326327/**328* Dispatch an event to event listeners.329*330* @param string Event type.331* @param dict Event parameters.332* @return void333*/334private function dispatchEvent($type, array $params = array()) {335$data = array(336'id' => $this->getDaemonID(),337'daemonClass' => $this->getDaemonClass(),338'childPID' => $this->getPID(),339) + $params;340341$event = new PhutilEvent($type, $data);342343try {344PhutilEventEngine::dispatchEvent($event);345} catch (Exception $ex) {346phlog($ex);347}348}349350private function annihilateProcessGroup() {351$pid = $this->getPID();352if ($pid) {353$pgid = posix_getpgid($pid);354if ($pgid) {355posix_kill(-$pgid, SIGTERM);356sleep($this->getKillDelay());357posix_kill(-$pgid, SIGKILL);358}359}360}361362private function startDaemonProcess() {363$this->logMessage('INIT', pht('Starting process.'));364365$this->deadline = time() + $this->getRequiredHeartbeatFrequency();366$this->heartbeat = time() + self::getHeartbeatEventFrequency();367$this->stdoutBuffer = '';368$this->hibernating = false;369370$future = $this->newExecFuture();371$this->future = $future;372373$pool = $this->getDaemonPool();374$overseer = $pool->getOverseer();375$overseer->addFutureToPool($future);376}377378private function didReadStdout($data) {379$this->stdoutBuffer .= $data;380while (true) {381$pos = strpos($this->stdoutBuffer, "\n");382if ($pos === false) {383break;384}385$message = substr($this->stdoutBuffer, 0, $pos);386$this->stdoutBuffer = substr($this->stdoutBuffer, $pos + 1);387388try {389$structure = phutil_json_decode($message);390} catch (PhutilJSONParserException $ex) {391$structure = array();392}393394switch (idx($structure, 0)) {395case PhutilDaemon::MESSAGETYPE_STDOUT:396$this->logMessage('STDO', idx($structure, 1));397break;398case PhutilDaemon::MESSAGETYPE_HEARTBEAT:399$this->deadline = time() + $this->getRequiredHeartbeatFrequency();400break;401case PhutilDaemon::MESSAGETYPE_BUSY:402if (!$this->busyEpoch) {403$this->busyEpoch = time();404}405break;406case PhutilDaemon::MESSAGETYPE_IDLE:407$this->busyEpoch = null;408break;409case PhutilDaemon::MESSAGETYPE_DOWN:410// The daemon is exiting because it doesn't have enough work and it411// is trying to scale the pool down. We should not restart it.412$this->shouldRestart = false;413$this->shouldShutdown = true;414break;415case PhutilDaemon::MESSAGETYPE_HIBERNATE:416$config = idx($structure, 1);417$duration = (int)idx($config, 'duration', 0);418$this->restartAt = time() + $duration;419$this->hibernating = true;420$this->busyEpoch = null;421$this->logMessage(422'ZZZZ',423pht(424'Process is preparing to hibernate for %s second(s).',425new PhutilNumber($duration)));426break;427default:428// If we can't parse this or it isn't a message we understand, just429// emit the raw message.430$this->logMessage('STDO', pht('<Malformed> %s', $message));431break;432}433}434}435436public function didReceiveNotifySignal($signo) {437$pid = $this->getPID();438if ($pid) {439posix_kill($pid, $signo);440}441}442443public function didReceiveReloadSignal($signo) {444$signame = phutil_get_signal_name($signo);445if ($signame) {446$sigmsg = pht(447'Reloading in response to signal %d (%s).',448$signo,449$signame);450} else {451$sigmsg = pht(452'Reloading in response to signal %d.',453$signo);454}455456$this->logMessage('RELO', $sigmsg, $signo);457458// This signal means "stop the current process gracefully, then launch459// a new identical process once it exits". This can be used to update460// daemons after code changes (the new processes will run the new code)461// without aborting any running tasks.462463// We SIGINT the daemon but don't set the shutdown flag, so it will464// naturally be restarted after it exits, as though it had exited after an465// unhandled exception.466467$pid = $this->getPID();468if ($pid) {469posix_kill($pid, SIGINT);470}471}472473public function didReceiveGracefulSignal($signo) {474$this->shouldShutdown = true;475$this->shouldRestart = false;476477$signame = phutil_get_signal_name($signo);478if ($signame) {479$sigmsg = pht(480'Graceful shutdown in response to signal %d (%s).',481$signo,482$signame);483} else {484$sigmsg = pht(485'Graceful shutdown in response to signal %d.',486$signo);487}488489$this->logMessage('DONE', $sigmsg, $signo);490491$pid = $this->getPID();492if ($pid) {493posix_kill($pid, SIGINT);494}495}496497public function didReceiveTerminateSignal($signo) {498$this->shouldShutdown = true;499$this->shouldRestart = false;500501$signame = phutil_get_signal_name($signo);502if ($signame) {503$sigmsg = pht(504'Shutting down in response to signal %s (%s).',505$signo,506$signame);507} else {508$sigmsg = pht('Shutting down in response to signal %s.', $signo);509}510511$this->logMessage('EXIT', $sigmsg, $signo);512$this->annihilateProcessGroup();513}514515private function logMessage($type, $message, $context = null) {516$this->getDaemonPool()->logMessage($type, $message, $context);517518$this->dispatchEvent(519self::EVENT_DID_LOG,520array(521'type' => $type,522'message' => $message,523'context' => $context,524));525}526527public function didExit() {528if ($this->shouldSendExitEvent) {529$this->dispatchEvent(self::EVENT_WILL_EXIT);530$this->shouldSendExitEvent = false;531}532533return $this;534}535536}537538539