Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/daemon/PhutilDaemonOverseer.php
12241 views
1
<?php
2
3
/**
4
* Oversees a daemon and restarts it if it fails.
5
*
6
* @task signals Signal Handling
7
*/
8
final 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(<<<EOHELP
43
**launch_daemon.php** [__options__] __daemon__
44
Launch and oversee an instance of __daemon__.
45
EOHELP
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
}
412
413