PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/src/infrastructure/daemon/PhutilDaemonOverseer.php

http://github.com/facebook/phabricator
PHP | 410 lines | 309 code | 76 blank | 25 comment | 38 complexity | 642fe34f76ed811533b8299d7c9a2543 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * Oversees a daemon and restarts it if it fails.
  4. *
  5. * @task signals Signal Handling
  6. */
  7. final class PhutilDaemonOverseer extends Phobject {
  8. private $argv;
  9. private static $instance;
  10. private $config;
  11. private $pools = array();
  12. private $traceMode;
  13. private $traceMemory;
  14. private $daemonize;
  15. private $log;
  16. private $libraries = array();
  17. private $modules = array();
  18. private $verbose;
  19. private $startEpoch;
  20. private $autoscale = array();
  21. private $autoscaleConfig = array();
  22. const SIGNAL_NOTIFY = 'signal/notify';
  23. const SIGNAL_RELOAD = 'signal/reload';
  24. const SIGNAL_GRACEFUL = 'signal/graceful';
  25. const SIGNAL_TERMINATE = 'signal/terminate';
  26. private $err = 0;
  27. private $inAbruptShutdown;
  28. private $inGracefulShutdown;
  29. private $futurePool;
  30. public function __construct(array $argv) {
  31. PhutilServiceProfiler::getInstance()->enableDiscardMode();
  32. $args = new PhutilArgumentParser($argv);
  33. $args->setTagline(pht('daemon overseer'));
  34. $args->setSynopsis(<<<EOHELP
  35. **launch_daemon.php** [__options__] __daemon__
  36. Launch and oversee an instance of __daemon__.
  37. EOHELP
  38. );
  39. $args->parseStandardArguments();
  40. $args->parse(
  41. array(
  42. array(
  43. 'name' => 'trace-memory',
  44. 'help' => pht('Enable debug memory tracing.'),
  45. ),
  46. array(
  47. 'name' => 'verbose',
  48. 'help' => pht('Enable verbose activity logging.'),
  49. ),
  50. array(
  51. 'name' => 'label',
  52. 'short' => 'l',
  53. 'param' => 'label',
  54. 'help' => pht(
  55. 'Optional process label. Makes "%s" nicer, no behavioral effects.',
  56. 'ps'),
  57. ),
  58. ));
  59. $argv = array();
  60. if ($args->getArg('trace')) {
  61. $this->traceMode = true;
  62. $argv[] = '--trace';
  63. }
  64. if ($args->getArg('trace-memory')) {
  65. $this->traceMode = true;
  66. $this->traceMemory = true;
  67. $argv[] = '--trace-memory';
  68. }
  69. $verbose = $args->getArg('verbose');
  70. if ($verbose) {
  71. $this->verbose = true;
  72. $argv[] = '--verbose';
  73. }
  74. $label = $args->getArg('label');
  75. if ($label) {
  76. $argv[] = '-l';
  77. $argv[] = $label;
  78. }
  79. $this->argv = $argv;
  80. if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
  81. fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");
  82. }
  83. $config = @file_get_contents('php://stdin');
  84. $config = id(new PhutilJSONParser())->parse($config);
  85. $this->libraries = idx($config, 'load');
  86. $this->log = idx($config, 'log');
  87. $this->daemonize = idx($config, 'daemonize');
  88. $this->config = $config;
  89. if (self::$instance) {
  90. throw new Exception(
  91. pht('You may not instantiate more than one Overseer per process.'));
  92. }
  93. self::$instance = $this;
  94. $this->startEpoch = time();
  95. if (!idx($config, 'daemons')) {
  96. throw new PhutilArgumentUsageException(
  97. pht('You must specify at least one daemon to start!'));
  98. }
  99. if ($this->log) {
  100. // NOTE: Now that we're committed to daemonizing, redirect the error
  101. // log if we have a `--log` parameter. Do this at the last moment
  102. // so as many setup issues as possible are surfaced.
  103. ini_set('error_log', $this->log);
  104. }
  105. if ($this->daemonize) {
  106. // We need to get rid of these or the daemon will hang when we TERM it
  107. // waiting for something to read the buffers. TODO: Learn how unix works.
  108. fclose(STDOUT);
  109. fclose(STDERR);
  110. ob_start();
  111. $pid = pcntl_fork();
  112. if ($pid === -1) {
  113. throw new Exception(pht('Unable to fork!'));
  114. } else if ($pid) {
  115. exit(0);
  116. }
  117. $sid = posix_setsid();
  118. if ($sid <= 0) {
  119. throw new Exception(pht('Failed to create new process session!'));
  120. }
  121. }
  122. $this->logMessage(
  123. 'OVER',
  124. pht(
  125. 'Started new daemon overseer (with PID "%s").',
  126. getmypid()));
  127. $this->modules = PhutilDaemonOverseerModule::getAllModules();
  128. $this->installSignalHandlers();
  129. }
  130. public function addLibrary($library) {
  131. $this->libraries[] = $library;
  132. return $this;
  133. }
  134. public function run() {
  135. $this->createDaemonPools();
  136. $future_pool = $this->getFuturePool();
  137. while (true) {
  138. if ($this->shouldReloadDaemons()) {
  139. $this->didReceiveSignal(SIGHUP);
  140. }
  141. $running_pools = false;
  142. foreach ($this->getDaemonPools() as $pool) {
  143. $pool->updatePool();
  144. if (!$this->shouldShutdown()) {
  145. if ($pool->isHibernating()) {
  146. if ($this->shouldWakePool($pool)) {
  147. $pool->wakeFromHibernation();
  148. }
  149. }
  150. }
  151. foreach ($pool->getFutures() as $future) {
  152. $future_pool->addFuture($future);
  153. }
  154. if ($pool->getDaemons()) {
  155. $running_pools = true;
  156. }
  157. }
  158. $this->updateMemory();
  159. if ($future_pool->hasFutures()) {
  160. $future_pool->resolve();
  161. } else {
  162. if (!$this->shouldShutdown()) {
  163. sleep(1);
  164. }
  165. }
  166. if (!$future_pool->hasFutures() && !$running_pools) {
  167. if ($this->shouldShutdown()) {
  168. break;
  169. }
  170. }
  171. }
  172. exit($this->err);
  173. }
  174. private function getFuturePool() {
  175. if (!$this->futurePool) {
  176. $pool = new FuturePool();
  177. // TODO: This only wakes if any daemons actually exit, or 1 second
  178. // passes. It would be a bit cleaner to wait on any I/O, but Futures
  179. // currently can't do that.
  180. $pool->getIteratorTemplate()
  181. ->setUpdateInterval(1);
  182. $this->futurePool = $pool;
  183. }
  184. return $this->futurePool;
  185. }
  186. private function createDaemonPools() {
  187. $configs = $this->config['daemons'];
  188. $forced_options = array(
  189. 'load' => $this->libraries,
  190. 'log' => $this->log,
  191. );
  192. foreach ($configs as $config) {
  193. $config = $forced_options + $config;
  194. $pool = PhutilDaemonPool::newFromConfig($config)
  195. ->setOverseer($this)
  196. ->setCommandLineArguments($this->argv);
  197. $this->pools[] = $pool;
  198. }
  199. }
  200. private function getDaemonPools() {
  201. return $this->pools;
  202. }
  203. private function updateMemory() {
  204. if (!$this->traceMemory) {
  205. return;
  206. }
  207. $this->logMessage(
  208. 'RAMS',
  209. pht(
  210. 'Overseer Memory Usage: %s KB',
  211. new PhutilNumber(memory_get_usage() / 1024, 1)));
  212. }
  213. public function logMessage($type, $message, $context = null) {
  214. $always_log = false;
  215. switch ($type) {
  216. case 'OVER':
  217. case 'SGNL':
  218. case 'PIDF':
  219. $always_log = true;
  220. break;
  221. }
  222. if ($always_log || $this->traceMode || $this->verbose) {
  223. error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message);
  224. }
  225. }
  226. /* -( Signal Handling )---------------------------------------------------- */
  227. /**
  228. * @task signals
  229. */
  230. private function installSignalHandlers() {
  231. $signals = array(
  232. SIGUSR2,
  233. SIGHUP,
  234. SIGINT,
  235. SIGTERM,
  236. );
  237. foreach ($signals as $signal) {
  238. pcntl_signal($signal, array($this, 'didReceiveSignal'));
  239. }
  240. }
  241. /**
  242. * @task signals
  243. */
  244. public function didReceiveSignal($signo) {
  245. $this->logMessage(
  246. 'SGNL',
  247. pht(
  248. 'Overseer ("%d") received signal %d ("%s").',
  249. getmypid(),
  250. $signo,
  251. phutil_get_signal_name($signo)));
  252. switch ($signo) {
  253. case SIGUSR2:
  254. $signal_type = self::SIGNAL_NOTIFY;
  255. break;
  256. case SIGHUP:
  257. $signal_type = self::SIGNAL_RELOAD;
  258. break;
  259. case SIGINT:
  260. // If we receive SIGINT more than once, interpret it like SIGTERM.
  261. if ($this->inGracefulShutdown) {
  262. return $this->didReceiveSignal(SIGTERM);
  263. }
  264. $this->inGracefulShutdown = true;
  265. $signal_type = self::SIGNAL_GRACEFUL;
  266. break;
  267. case SIGTERM:
  268. // If we receive SIGTERM more than once, terminate abruptly.
  269. $this->err = 128 + $signo;
  270. if ($this->inAbruptShutdown) {
  271. exit($this->err);
  272. }
  273. $this->inAbruptShutdown = true;
  274. $signal_type = self::SIGNAL_TERMINATE;
  275. break;
  276. default:
  277. throw new Exception(
  278. pht(
  279. 'Signal handler called with unknown signal type ("%d")!',
  280. $signo));
  281. }
  282. foreach ($this->getDaemonPools() as $pool) {
  283. $pool->didReceiveSignal($signal_type, $signo);
  284. }
  285. }
  286. /* -( Daemon Modules )----------------------------------------------------- */
  287. private function getModules() {
  288. return $this->modules;
  289. }
  290. private function shouldReloadDaemons() {
  291. $modules = $this->getModules();
  292. $should_reload = false;
  293. foreach ($modules as $module) {
  294. try {
  295. // NOTE: Even if one module tells us to reload, we call the method on
  296. // each module anyway to make calls a little more predictable.
  297. if ($module->shouldReloadDaemons()) {
  298. $this->logMessage(
  299. 'RELO',
  300. pht(
  301. 'Reloading daemons (triggered by overseer module "%s").',
  302. get_class($module)));
  303. $should_reload = true;
  304. }
  305. } catch (Exception $ex) {
  306. phlog($ex);
  307. }
  308. }
  309. return $should_reload;
  310. }
  311. private function shouldWakePool(PhutilDaemonPool $pool) {
  312. $modules = $this->getModules();
  313. $should_wake = false;
  314. foreach ($modules as $module) {
  315. try {
  316. if ($module->shouldWakePool($pool)) {
  317. $this->logMessage(
  318. 'WAKE',
  319. pht(
  320. 'Waking pool "%s" (triggered by overseer module "%s").',
  321. $pool->getPoolLabel(),
  322. get_class($module)));
  323. $should_wake = true;
  324. }
  325. } catch (Exception $ex) {
  326. phlog($ex);
  327. }
  328. }
  329. return $should_wake;
  330. }
  331. private function shouldShutdown() {
  332. return $this->inGracefulShutdown || $this->inAbruptShutdown;
  333. }
  334. }