Master::shutdown()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
namespace PHPDaemon\Thread;
3
4
use PHPDaemon\Core\Daemon;
5
use PHPDaemon\Core\Debug;
6
use PHPDaemon\Core\EventLoop;
7
use PHPDaemon\Core\Timer;
8
use PHPDaemon\FS\FileSystem;
9
use PHPDaemon\Structures\StackCallbacks;
10
11
/**
12
 * Implementation of the master thread
13
 *
14
 * @package Core
15
 *
16
 * @author  Vasily Zorin <[email protected]>
17
 */
18
class Master extends Generic
19
{
20
21
    /** @var bool */
22
    public $delayedSigReg = true;
23
    /** @var bool */
24
    public $breakMainLoop = false;
25
    /** @var bool */
26
    public $reload = false;
27
    /** @var int */
28
    public $connCounter = 0;
29
    /** @var Collection */
30
    public $workers;
31
    /** @var Collection */
32
    public $ipcthreads;
33
    /** @var */
34
    public $lastMpmActionTs;
35
    /** @var int */
36
    public $minMpmActionInterval = 1; // in seconds
37
    protected $timerCb;
38
39
    /**
40
     * @var StackCallbacks
41
     */
42
    protected $callbacks;
43
44
    /**
45
     * Runtime of Master process
46
     * @return void
47
     */
48
    protected function run()
49
    {
50
        Daemon::$process = $this;
51
52
        $this->prepareSystemEnv();
53
        class_exists('Timer'); // ensure loading this class
54
        gc_enable();
55
56
        $this->callbacks = new StackCallbacks;
57
58
        /*
59
         * @todo This line must be commented according to current libevent binding implementation.
60
         * May be uncommented in future.
61
         */
62
        //EventLoop::init()
63
64
        if (EventLoop::$instance) {
65
            $this->registerEventSignals();
66
        } else {
67
            $this->registerSignals();
68
        }
69
70
        $this->workers = new Collection();
71
        $this->collections['workers'] = $this->workers;
72
        $this->ipcthreads = new Collection;
73
        $this->collections['ipcthreads'] = $this->ipcthreads;
74
75
        Daemon::$appResolver->preload(true);
76
77
        $this->spawnIPCThread();
78
        $this->spawnWorkers(
79
            min(
80
                Daemon::$config->startworkers->value,
81
                Daemon::$config->maxworkers->value
82
            )
83
        );
84
        $this->timerCb = function ($event) use (&$cbs) {
85
            static $c = 0;
86
87
            ++$c;
88
89
            if ($c > 0xFFFFF) {
90
                $c = 1;
91
            }
92
93
            if (($c % 10 == 0)) {
94
                gc_collect_cycles();
95
            }
96
97
            if (!$this->lastMpmActionTs || ((microtime(true) - $this->lastMpmActionTs) > $this->minMpmActionInterval)) {
98
                $this->callMPM();
99
            }
100
            if ($event) {
101
                $event->timeout();
102
            }
103
        };
104
105
        if (EventLoop::$instance) { // we are using libevent in Master
106
            Timer::add($this->timerCb, 1e6 * Daemon::$config->mpmdelay->value, 'MPM');
107
            EventLoop::$instance->run();
108
        } else { // we are NOT using libevent in Master
109
            $lastTimerCall = microtime(true);
110
            $func = $this->timerCb;
111
            while (!$this->breakMainLoop) {
112
                $this->callbacks->executeAll($this);
113
                if (microtime(true) > $lastTimerCall + Daemon::$config->mpmdelay->value) {
114
                    $func(null);
115
                    $lastTimerCall = microtime(true);
116
                }
117
                $this->sigwait();
118
            }
119
        }
120
    }
121
122
    /**
123
     * Log something
124
     * @param string - Message.
125
     * @param string $message
126
     * @return void
127
     */
128
    public function log($message)
129
    {
130
        Daemon::log('M#' . $this->pid . ' ' . $message);
131
    }
132
133
    /**
134
     * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
135
     */
136
    protected function callMPM()
137
    {
138
        $state = Daemon::getStateOfWorkers();
139
        if (isset(Daemon::$config->mpm->value) && is_callable($func = Daemon::$config->mpm->value)) {
0 ignored issues
show
Bug introduced by
The property mpm does not seem to exist in PHPDaemon\Config\_Object.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
140
            $func($this, $state);
141
        }
142
143
        $upToMinWorkers = Daemon::$config->minworkers->value - $state['alive'];
144
        $upToMaxWorkers = Daemon::$config->maxworkers->value - $state['alive'];
145
        $upToMinSpareWorkers = Daemon::$config->minspareworkers->value - $state['idle'];
146
        if ($upToMinSpareWorkers > $upToMaxWorkers) {
147
            $upToMinSpareWorkers = $upToMaxWorkers;
148
        }
149
        $n = max($upToMinSpareWorkers, $upToMinWorkers);
150
        if ($n > 0) {
151
            //Daemon::log('minspareworkers = '.Daemon::$config->minspareworkers->value);
152
            //Daemon::log('maxworkers = '.Daemon::$config->maxworkers->value);
153
            //Daemon::log('maxspareworkers = '.Daemon::$config->maxspareworkers->value);
154
            //Daemon::log(json_encode($state));
155
            //Daemon::log('upToMinSpareWorkers = ' . $upToMinSpareWorkers . '   upToMinWorkers = ' . $upToMinWorkers);
156
            Daemon::log('Spawning ' . $n . ' worker(s)');
157
            $this->spawnWorkers($n);
158
            return $n;
159
        }
160
161
        $a = ['default' => 0];
162
        if (Daemon::$config->maxspareworkers->value > 0) {
163
            // if MaxSpareWorkers enabled, we have to stop idle workers, keeping in mind the MinWorkers
164
            $a['downToMaxSpareWorkers'] = min(
165
                $state['idle'] - Daemon::$config->maxspareworkers->value, // downToMaxSpareWorkers
166
                $state['alive'] - Daemon::$config->minworkers->value //downToMinWorkers
167
            );
168
        }
169
        $a['downToMaxWorkers'] = $state['alive'] - $state['reloading'] - Daemon::$config->maxworkers->value;
170
        $n = max($a);
171
        if ($n > 0) {
172
            //Daemon::log('down = ' . json_encode($a));
173
            //Daemon::log(json_encode($state));
174
            Daemon::log('Stopping ' . $n . ' worker(s)');
175
            $this->stopWorkers($n);
176
            return -$n;
177
        }
178
        return 0;
179
    }
180
181
    /**
182
     * Setup settings on start.
183
     * @return void
184
     */
185
    protected function prepareSystemEnv()
186
    {
187
        register_shutdown_function(function () {
188
            if ($this->pid != posix_getpid()) {
189
                return;
190
            }
191
            if ($this->shutdown === true) {
192
                return;
193
            }
194
            $this->log('Unexcepted shutdown.');
195
            $this->shutdown();
196
        });
197
198
        posix_setsid();
199
        proc_nice(Daemon::$config->masterpriority->value);
200 View Code Duplication
        if (!Daemon::$config->verbosetty->value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
            fclose(STDIN);
202
            fclose(STDOUT);
203
            fclose(STDERR);
204
        }
205
206
        $this->setTitle(
207
            Daemon::$runName . ': master process'
208
            . (Daemon::$config->pidfile->value !== Daemon::$config->pidfile->defaultValue ? ' (' . Daemon::$config->pidfile->value . ')' : '')
209
        );
210
    }
211
212
    /**
213
     * Reload worker by internal id
214
     * @param integer - Id of worker
215
     * @param integer $id
216
     * @return void
217
     */
218
    public function reloadWorker($id)
219
    {
220
        if (isset($this->workers->threads[$id])) {
221
            if (!$this->workers->threads[$id]->reloaded) {
0 ignored issues
show
Bug introduced by
The property reloaded does not seem to exist in PHPDaemon\Thread\Generic.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
222
                Daemon::$process->log('Spawning worker-replacer for reloaded worker #' . $id);
223
                $this->spawnWorkers(1);
224
                $this->workers->threads[$id]->reloaded = true;
0 ignored issues
show
Documentation introduced by
The property reloaded does not exist on object<PHPDaemon\Thread\Generic>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
225
            }
226
        }
227
    }
228
229
    /**
230
     * Spawn new worker processes
231
     * @param $n - integer - number of workers to spawn
232
     * @return boolean - success
233
     */
234
    protected function spawnWorkers($n)
235
    {
236
        if (FileSystem::$supported) {
237
            eio_event_loop();
238
        }
239
        $n = (int)$n;
240
241
        for ($i = 0; $i < $n; ++$i) {
242
            $thread = new Worker;
243
            $this->workers->push($thread);
244 View Code Duplication
            $this->callbacks->push(function ($self) use ($thread) {
0 ignored issues
show
Unused Code introduced by
The parameter $self is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
245
                // @check - is it possible to run iterate of main event loop without child termination?
246
                $thread->start();
247
                $pid = $thread->getPid();
248
                if ($pid < 0) {
249
                    Daemon::$process->log('could not fork worker');
250
                } elseif ($pid === 0) { // worker
251
                    Daemon::log('Unexcepted execution return to outside of Thread->start()');
252
                    exit;
253
                }
254
            });
255
        }
256
        if ($n > 0) {
257
            $this->lastMpmActionTs = microtime(true);
258
            if (EventLoop::$instance) {
259
                EventLoop::$instance->interrupt();
260
            }
261
        }
262
        return true;
263
    }
264
265
    /**
266
     * Spawn IPC process
267
     * @param $n - integer - number of workers to spawn
268
     * @return boolean - success
269
     */
270
    protected function spawnIPCThread()
271
    {
272
        if (FileSystem::$supported) {
273
            eio_event_loop();
274
        }
275
        $thread = new IPC;
276
        $this->ipcthreads->push($thread);
277
278 View Code Duplication
        $this->callbacks->push(function ($self) use ($thread) {
0 ignored issues
show
Unused Code introduced by
The parameter $self is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
            $thread->start();
280
            $pid = $thread->getPid();
281
            if ($pid < 0) {
282
                Daemon::$process->log('could not fork IPCThread');
283
            } elseif ($pid === 0) { // worker
284
                $this->log('Unexcepted execution return to outside of Thread->start()');
285
                exit;
286
            }
287
        });
288
        if (EventLoop::$instance) {
289
            EventLoop::$instance->interrupt();
290
        }
291
        return true;
292
    }
293
294
    /**
295
     * Stop the workers
296
     * @param $n - integer - number of workers to stop
297
     * @return boolean - success
298
     */
299
    protected function stopWorkers($n = 1)
300
    {
301
        Daemon::log('--' . $n . '-- ' . Debug::backtrace() . '-----');
302
303
        $n = (int)$n;
304
        $i = 0;
305
306
        foreach ($this->workers->threads as &$w) {
307
            if ($i >= $n) {
308
                break;
309
            }
310
311
            if ($w->shutdown) {
312
                continue;
313
            }
314
315
            if ($w->reloaded) {
0 ignored issues
show
Bug introduced by
The property reloaded does not seem to exist in PHPDaemon\Thread\Generic.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
316
                continue;
317
            }
318
319
            $w->stop();
320
            ++$i;
321
        }
322
323
        $this->lastMpmActionTs = microtime(true);
324
        return true;
325
    }
326
327
    /**
328
     * Called when master is going to shutdown
329
     * @param integer System singal's number
330
     * @return void
331
     */
332
    protected function shutdown(int $signo = null)
333
    {
334
        if ($signo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $signo of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
335
            $this->signalToChildren($signo);
336
        }
337
        $this->shutdown = true;
338
        $this->waitAll();
339
        Daemon::$shm_wstate->delete();
340
        file_put_contents(Daemon::$config->pidfile->value, '');
341
        posix_kill(getmypid(), SIGKILL);
342
        exit(0);
343
    }
344
345
    /**
346
     * Handler for the SIGCHLD (child is dead) signal in master process.
347
     * @return void
348
     */
349
    protected function sigchld()
350
    {
351
        if (Daemon::$config->logsignals->value) {
352
            $this->log('Caught SIGCHLD.');
353
        }
354
355
        parent::sigchld();
356
    }
357
358
    /**
359
     * Handler for the SIGINT (shutdown) signal in master process. Shutdown.
360
     * @return void
361
     */
362
    protected function sigint()
363
    {
364
        if (Daemon::$config->logsignals->value) {
365
            $this->log('Caught SIGINT.');
366
        }
367
        $this->shutdown(SIGKILL);
368
    }
369
370
    /**
371
     * @param $signo
372
     */
373
    public function signalToChildren($signo)
374
    {
375
        foreach ($this->collections as $col) {
376
            $col->signal($signo);
377
        }
378
    }
379
380
    /**
381
     * Handler for the SIGTERM (shutdown) signal in master process
382
     * @return void
383
     */
384
    protected function sigterm()
385
    {
386
        if (Daemon::$config->logsignals->value) {
387
            $this->log('Caught SIGTERM.');
388
        }
389
390
        $this->shutdown(SIGTERM);
391
    }
392
393
    /**
394
     * Handler for the SIGQUIT signal in master process
395
     * @return void
396
     */
397
    protected function sigquit()
398
    {
399
        if (Daemon::$config->logsignals->value) {
400
            $this->log('Caught SIGQUIT.');
401
        }
402
403
        $this->shutdown(SIGQUIT);
404
    }
405
406
    /**
407
     * Handler for the SIGTSTP (graceful stop all workers) signal in master process
408
     * @return void
409
     */
410
    protected function sigtstp()
411
    {
412
        if (Daemon::$config->logsignals->value) {
413
            $this->log('Caught SIGTSTP (graceful stop all workers).');
414
        }
415
416
        $this->shutdown(SIGTSTP);
417
    }
418
419
    /**
420
     * Handler for the SIGHUP (reload config) signal in master process
421
     * @return void
422
     */
423 View Code Duplication
    protected function sighup()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
424
    {
425
        if (Daemon::$config->logsignals->value) {
426
            $this->log('Caught SIGHUP (reload config).');
427
        }
428
429
        if (isset(Daemon::$config->configfile)) {
430
            Daemon::loadConfig(Daemon::$config->configfile->value);
431
        }
432
433
        $this->signalToChildren(SIGHUP);
434
    }
435
436
    /**
437
     * Handler for the SIGUSR1 (re-open log-file) signal in master process
438
     * @return void
439
     */
440
    protected function sigusr1()
441
    {
442
        if (Daemon::$config->logsignals->value) {
443
            $this->log('Caught SIGUSR1 (re-open log-file).');
444
        }
445
446
        Daemon::openLogs();
447
        $this->signalToChildren(SIGUSR1);
448
    }
449
450
    /**
451
     * Handler for the SIGUSR2 (graceful restart all workers) signal in master process
452
     * @return void
453
     */
454
    protected function sigusr2()
455
    {
456
        if (Daemon::$config->logsignals->value) {
457
            $this->log('Caught SIGUSR2 (graceful restart all workers).');
458
        }
459
        $this->signalToChildren(SIGUSR2);
460
    }
461
462
    /**
463
     * Handler for the SIGTTIN signal in master process
464
     * Used as "ping" signal
465
     * @return void
466
     */
467
    protected function sigttin()
468
    {
469
    }
470
471
    /**
472
     * Handler for the SIGXSFZ signal in master process
473
     * @return void
474
     */
475
    protected function sigxfsz()
476
    {
477
        $this->log('Caught SIGXFSZ.');
478
    }
479
480
    /**
481
     * Handler for non-known signals
482
     * @return void
483
     */
484 View Code Duplication
    protected function sigunknown($signo)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
    {
486
        if (isset(Generic::$signals[$signo])) {
487
            $sig = Generic::$signals[$signo];
488
        } else {
489
            $sig = 'UNKNOWN';
490
        }
491
        $this->log('Caught signal #' . $signo . ' (' . $sig . ').');
492
    }
493
}
494