Passed
Push — develop ( 91ba7c...358cc2 )
by Mathieu
03:54
created

Redis::signalHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 9
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 15
ccs 0
cts 10
cp 0
crap 6
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Suricate\Worker;
6
7
use Suricate\Suricate;
8
9
/**
10
 * Redis worker class
11
 */
12
class Redis
13
{
14
    /** @var string $redisHost Redis host name */
15
    protected $redisHost;
16
17
    /** @var string $redisPort Redis port number */
18
    protected $redisPort;
19
20
    /** @var int $maxChildren Number of children to launch */
21
    protected $maxChildren = 1;
22
23
    /** @var string $redisFifoName Redis key to listen to */
24
    protected $redisFifoName;
25
26
    /** @var int $redisFifoTimeout Redis blpop timeout value */
27
    protected $redisFifoTimeout = 2;
28
29
    private $childSlots = [];
30
31
    /** @var Suricate\Logger $logger */
0 ignored issues
show
Bug introduced by
The type Suricate\Suricate\Logger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
    private $logger;
33
    private $logPrefix = 'FATHER';
34
35
    public function __construct()
36
    {
37
        if ($this->redisHost === '') {
38
            throw new \Exception("Redis host is not set");
39
        }
40
41
        if ($this->redisPort === '') {
42
            throw new \Exception("Redis port is not set");
43
        }
44
45
        if ($this->redisFifoName === '') {
46
            throw new \Exception("Redis fifo name is not set");
47
        }
48
49
        $this->logger = Suricate::Logger();
0 ignored issues
show
Documentation Bug introduced by
It seems like Suricate\Suricate::Logger() of type Suricate\Logger is incompatible with the declared type Suricate\Suricate\Logger of property $logger.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
50
    }
51
52
    /**
53
     * Worker log helper
54
     *
55
     * @param string $message
56
     * @return void
57
     */
58
    protected function log($message)
59
    {
60
        $this->logger->info('[' . $this->logPrefix . '] ' . $message);
61
    }
62
63
    /**
64
     * dummy function to hande an incoming job
65
     * must be overriden in inherited class
66
     *
67
     * @param array|null $payload
68
     * @return void
69
     */
70
    public function handleJob(?array $payload)
71
    {
72
        $this->logger->fatal(
73
            'received ' . json_encode($payload) . 'but no handle job defined'
74
        );
75
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
76
    }
77
78
    /**
79
     * Worker main run function
80
     *
81
     * @return void
82
     */
83
    public function run()
84
    {
85
        set_time_limit(0);
86
        posix_setsid();
87
        set_error_handler([$this, "errorHandler"]);
88
89
        $this->log(
90
            sprintf(
91
                'Starting worker, redis: %s:%s, queue name %s',
92
                $this->redisHost,
93
                $this->redisPort,
94
                $this->redisFifoName
95
            )
96
        );
97
        $this->log('Launching childrens');
98
99
        // Forking children
100
        for ($i = 0; $i < $this->maxChildren; $i++) {
101
            $pid = pcntl_fork();
102
            if ($pid == -1) {
103
                die('Impossible to fork');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
104
            } elseif ($pid > 0) {
105
                $father = true;
106
                $this->childSlots[$i]['pid'] = $pid;
107
                $this->childSlots[$i]['start_time'] = time();
108
                $this->log("Launching child " . ($i + 1) . " with PID " . $pid);
109
            } elseif ($pid == 0) {
110
                $childNum = $i + 1;
111
                $this->logPrefix = 'CHILD_' . $childNum;
112
                $father = false;
113
                $this->log("Launched");
114
                break;
115
            }
116
        }
117
        if ($father) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $father does not seem to be defined for all execution paths leading up to this point.
Loading history...
118
            $this->startFather();
119
        }
120
        if (!$father) {
121
            $this->listen();
122
        }
123
    }
124
125
    public function shutdown()
126
    {
127
        $this->signalHandler(SIGTERM);
128
    }
129
130
    /**
131
     * Father process main loop
132
     * declare sig handler and enter in an infinite loop
133
     *
134
     * @return void
135
     */
136
    private function startFather()
137
    {
138
        register_shutdown_function([$this, 'shutdown']);
139
140
        declare(ticks=1);
141
        pcntl_signal(SIGINT, [$this, 'signalHandler']);
142
        pcntl_signal(SIGTERM, [$this, 'signalHandler']);
143
        pcntl_signal(SIGHUP, [$this, 'signalHandler']);
144
        pcntl_signal(SIGUSR1, [$this, 'signalHandler']);
145
        pcntl_signal(SIGCHLD, [$this, 'signalHandler']);
146
        while (true) {
147
            sleep(2);
148
            if (!$this->checkIncludedFiles()) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->checkIncludedFiles() targeting Suricate\Worker\Redis::checkIncludedFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
149
                exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
150
            }
151
        }
152
    }
153
154
    /**
155
     * Children listening function
156
     * connect to redis and wait for a job
157
     *
158
     * @return void
159
     */
160
    private function listen()
161
    {
162
        $this->log('Connecting to redis');
163
164
        $redisSrv = new \Predis\Client([
0 ignored issues
show
Bug introduced by
The type Predis\Client was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
165
            'scheme' => 'tcp',
166
            'host' => $this->redisHost,
167
            'port' => $this->redisPort,
168
            'read_write_timeout' => 0
169
        ]);
170
        $this->setIncludedFiles();
171
172
        while (true) {
173
            $result = $redisSrv->blpop(
174
                $this->redisFifoName,
175
                $this->redisFifoTimeout
176
            );
177
            if ($result) {
178
                $this->log('Received job ' . json_encode($result[1]));
179
                $this->handleJob($result[1]);
180
            }
181
182
            if (!$this->checkIncludedFiles()) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->checkIncludedFiles() targeting Suricate\Worker\Redis::checkIncludedFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
183
                exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
184
            }
185
        }
186
    }
187
188
    /**
189
     * Store modification time of all included files of the worker
190
     * to detect modification and do restart
191
     *
192
     * @return void
193
     */
194
    private function setIncludedFiles()
195
    {
196
        $this->includedFiles = [];
0 ignored issues
show
Bug Best Practice introduced by
The property includedFiles does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
197
        $allIncludedFiles = get_included_files();
198
199
        foreach ($allIncludedFiles as $filename) {
200
            $includeVersion = filemtime($filename);
201
            $this->includedFiles[$filename] = $includeVersion;
202
        }
203
    }
204
205
    /**
206
     * Compare included files modification times with original ones
207
     *
208
     * @return void
209
     */
210
    private function checkIncludedFiles()
211
    {
212
        clearstatcache();
213
        $allIncludedFiles = get_included_files();
214
        foreach ($allIncludedFiles as $filename) {
215
            $includeVersion = filemtime($filename);
216
            if (isset($this->includedFiles[$filename])) {
217
                $version = $this->includedFiles[$filename];
218
219
                if ($includeVersion > $version) {
220
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type void.
Loading history...
221
                }
222
            } else {
223
                $this->includedFiles[$filename] = $includeVersion;
0 ignored issues
show
Bug Best Practice introduced by
The property includedFiles does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
224
            }
225
        }
226
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type void.
Loading history...
227
    }
228
    /**
229
     * signal handler, kill children when father receive signal
230
     *
231
     * @param int $signalNumber
232
     * @return void
233
     */
234
    private function signalHandler($signalNumber)
235
    {
236
        $this->log('received signal ' . $signalNumber);
237
        foreach ($this->childSlots as $index => $child) {
238
            $this->log(
239
                "Child[" . $index . "] Sending SIGTERM TO " . $child['pid']
240
            );
241
            posix_kill($child['pid'], SIGTERM);
242
            pcntl_wait($status);
243
            $this->log(
244
                "Child[" . $index . "] returned with status[" . $status . "]"
245
            );
246
        }
247
248
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
249
    }
250
251
    private function errorHandler(
252
        $errNumber,
253
        $errMessage,
254
        $filename,
255
        $lineNumber,
256
        $vars
257
    ) {
258
        $this->log(
259
            'Error occured: ' .
260
                $errMessage .
261
                '/' .
262
                $errNumber .
263
                ', filename: ' .
264
                $filename .
265
                ' on line: ' .
266
                $lineNumber .
267
                '. vars : ' .
268
                json_encode($vars)
269
        );
270
    }
271
}
272