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