@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
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}