Redis::startFather()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 11
c 1
b 0
f 1
nc 3
nop 0
dl 0
loc 14
ccs 0
cts 11
cp 0
crap 12
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Suricate\Worker;
6
7
use Suricate\Suricate;
8
use Exception;
9
use Predis\Client;
10
11
/**
12
 * Redis worker class
13
 */
14
class Redis
15
{
16
    /** @var string $redisHost Redis host name */
17
    protected $redisHost;
18
19
    /** @var string $redisPort Redis port number */
20
    protected $redisPort;
21
22
    /** @var int $maxChildren Number of children to launch */
23
    protected $maxChildren = 1;
24
25
    /** @var string $redisFifoName Redis key to listen to */
26
    protected $redisFifoName;
27
28
    /** @var int $redisFifoTimeout Redis blpop timeout value */
29
    protected $redisFifoTimeout = 2;
30
31
    private $childSlots = [];
32
    private $includedFiles = [];
33
34
    /** @var \Suricate\Logger $logger */
35
    private $logger;
36
    private $logPrefix = 'FATHER';
37
38
    public function __construct()
39
    {
40
        if ($this->redisHost === '') {
41
            throw new Exception("Redis host is not set");
42
        }
43
44
        if ($this->redisPort === '') {
45
            throw new Exception("Redis port is not set");
46
        }
47
48
        if ($this->redisFifoName === '') {
49
            throw new Exception("Redis fifo name is not set");
50
        }
51
52
        $this->logger = Suricate::Logger();
53
    }
54
55
    /**
56
     * Worker log helper
57
     *
58
     * @param string $message
59
     * @return void
60
     */
61
    protected function log($message)
62
    {
63
        $this->logger->info('[' . $this->logPrefix . '] ' . $message);
64
    }
65
66
    /**
67
     * dummy function to hande an incoming job
68
     * must be overriden in inherited class
69
     *
70
     * @param array|null $payload
71
     * @return void
72
     */
73
    public function handleJob(?array $payload)
74
    {
75
        $this->logger->fatal(
76
            'received ' . json_encode($payload) . 'but no handle job defined'
77
        );
78
        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...
79
    }
80
81
    /**
82
     * Enqueue a job into fifo
83
     *
84
     * @param mixed $payload
85
     * @return void
86
     */
87
    public function enqueue($payload)
88
    {
89
        $redisSrv = new Client([
90
            'scheme' => 'tcp',
91
            'host' => $this->redisHost,
92
            'port' => $this->redisPort,
93
            'read_write_timeout' => $this->redisFifoTimeout
94
        ]);
95
96
        $redisSrv->rpush($this->redisFifoName, json_encode($payload));
97
    }
98
    /**
99
     * Worker main run function
100
     *
101
     * @return void
102
     */
103
    public function run()
104
    {
105
        set_time_limit(0);
106
        posix_setsid();
107
        set_error_handler([$this, "errorHandler"]);
108
109
        $this->log(
110
            sprintf(
111
                'Starting worker, redis: %s:%s, queue name %s',
112
                $this->redisHost,
113
                $this->redisPort,
114
                $this->redisFifoName
115
            )
116
        );
117
        $this->log('Launching childrens');
118
        $father = true;
119
        // Forking children
120
        for ($i = 0; $i < $this->maxChildren; $i++) {
121
            $pid = pcntl_fork();
122
            if ($pid == -1) {
123
                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...
124
            } elseif ($pid > 0) {
125
                $father = true;
126
                $this->childSlots[$i]['pid'] = $pid;
127
                $this->childSlots[$i]['start_time'] = time();
128
                $this->log("Launching child " . ($i + 1) . " with PID " . $pid);
129
            } elseif ($pid == 0) {
130
                $childNum = $i + 1;
131
                $this->logPrefix = 'CHILD_' . $childNum;
132
                $father = false;
133
                $this->log("Launched");
134
                break;
135
            }
136
        }
137
        if ($father) {
138
            $this->startFather();
139
        }
140
        if (!$father) {
0 ignored issues
show
introduced by
The condition $father is always false.
Loading history...
141
            $this->listen();
142
        }
143
    }
144
145
    public function shutdown()
146
    {
147
        $this->signalHandler(SIGTERM);
148
    }
149
150
    /**
151
     * Father process main loop
152
     * declare sig handler and enter in an infinite loop
153
     *
154
     * @return void
155
     */
156
    private function startFather()
157
    {
158
        register_shutdown_function([$this, 'shutdown']);
159
160
        declare(ticks=1);
161
        pcntl_signal(SIGINT, [$this, 'signalHandler']);
162
        pcntl_signal(SIGTERM, [$this, 'signalHandler']);
163
        pcntl_signal(SIGHUP, [$this, 'signalHandler']);
164
        pcntl_signal(SIGUSR1, [$this, 'signalHandler']);
165
        pcntl_signal(SIGCHLD, [$this, 'signalHandler']);
166
        while (true) {
167
            sleep(2);
168
            if (!$this->checkIncludedFiles()) {
169
                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...
170
            }
171
        }
172
    }
173
174
    /**
175
     * Children listening function
176
     * connect to redis and wait for a job
177
     *
178
     * @return void
179
     */
180
    private function listen()
181
    {
182
        $this->log('Connecting to redis');
183
184
        $redisSrv = new Client([
185
            'scheme' => 'tcp',
186
            'host' => $this->redisHost,
187
            'port' => $this->redisPort,
188
            'read_write_timeout' => 0
189
        ]);
190
        $this->setIncludedFiles();
191
192
        while (true) {
193
            $result = $redisSrv->blpop(
194
                [$this->redisFifoName],
195
                $this->redisFifoTimeout
196
            );
197
            if ($result) {
198
                $this->log('Received job ' . json_encode($result[1]));
199
                $this->handleJob($result[1]);
200
            }
201
202
            if (!$this->checkIncludedFiles()) {
203
                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...
204
            }
205
        }
206
    }
207
208
    /**
209
     * Store modification time of all included files of the worker
210
     * to detect modification and do restart
211
     *
212
     * @return void
213
     */
214
    private function setIncludedFiles()
215
    {
216
        $this->includedFiles = [];
217
        $allIncludedFiles = get_included_files();
218
219
        foreach ($allIncludedFiles as $filename) {
220
            $includeVersion = filemtime($filename);
221
            $this->includedFiles[$filename] = $includeVersion;
222
        }
223
    }
224
225
    /**
226
     * Compare included files modification times with original ones
227
     *
228
     * @return bool
229
     */
230
    private function checkIncludedFiles()
231
    {
232
        clearstatcache();
233
        $allIncludedFiles = get_included_files();
234
        foreach ($allIncludedFiles as $filename) {
235
            $includeVersion = filemtime($filename);
236
            if (isset($this->includedFiles[$filename])) {
237
                $version = $this->includedFiles[$filename];
238
239
                if ($includeVersion > $version) {
240
                    return false;
241
                }
242
            } else {
243
                $this->includedFiles[$filename] = $includeVersion;
244
            }
245
        }
246
        return true;
247
    }
248
    /**
249
     * signal handler, kill children when father receive signal
250
     *
251
     * @param int $signalNumber
252
     * @return void
253
     *
254
     * @SuppressWarnings(PHPMD.ExitExpression)
255
     */
256
    private function signalHandler($signalNumber)
257
    {
258
        $this->log('received signal ' . $signalNumber);
259
        foreach ($this->childSlots as $index => $child) {
260
            $this->log(
261
                "Child[" . $index . "] Sending SIGTERM TO " . $child['pid']
262
            );
263
            posix_kill($child['pid'], SIGTERM);
264
265
            pcntl_wait($status);
266
            $this->log(
267
                "Child[" . $index . "] returned with status[" . $status . "]"
268
            );
269
        }
270
271
        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...
272
    }
273
274
    /**
275
     * Redis worker error handler
276
     *
277
     * @param int $errNumber
278
     * @param string $errMessage
279
     * @param string  $filename
280
     * @param int $lineNumber
281
     * @param array $vars
282
     * @return boolean
283
     */
284
    public function errorHandler(
285
        $errNumber,
286
        $errMessage,
287
        $filename,
288
        $lineNumber,
289
        $vars = []
290
    ): bool {
291
        $this->log(
292
            'Error occured: ' .
293
                $errMessage .
294
                '/' .
295
                $errNumber .
296
                ', filename: ' .
297
                $filename .
298
                ' on line: ' .
299
                $lineNumber .
300
                '. vars : ' .
301
                json_encode($vars)
302
        );
303
304
        return false;
305
    }
306
}
307