Passed
Push — master ( 34f3fe...502343 )
by Ioannes
08:17
created

Cron::isActualJob()   C

Complexity

Conditions 15
Paths 108

Size

Total Lines 41
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
c 1
b 0
f 0
dl 0
loc 41
rs 5.85
cc 15
nc 108
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace App\BxConsole;
3
4
use Doctrine\Common\Annotations\AnnotationReader;
5
use Symfony\Component\Console\Helper\Table;
6
use Symfony\Component\Console\Helper\TableCell;
7
use Symfony\Component\Console\Helper\TableSeparator;
8
use Symfony\Component\Console\Input\ArrayInput;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\ConsoleOutputInterface;
12
use Symfony\Component\Console\Output\OutputInterface;
13
use App\BxConsole\Annotations\Agent;
14
15
class Cron extends BxCommand {
16
17
    use LockableTrait;
18
19
    const EXEC_STATUS_SUCCESS = 'SUCCESS';
20
    const EXEC_STATUS_ERROR = 'ERROR';
21
    const EXEC_STATUS_WORK = 'WORK';
22
23
    const RESTART_TIME = 3600;
24
25
    const SORT_NAME = 'name';
26
    const SORT_TIME = 'time';
27
28
    private $minAgentPeriod;
29
30
    protected function configure() {
31
32
        $this->setName('system:cron')
33
            ->setDescription('Job scheduler for application commands')
34
            ->addOption('status', 's', InputOption::VALUE_NONE, 'Show BX_CRONTAB status table')
35
            ->addOption('bytime', 't', InputOption::VALUE_NONE, 'Sort status table by exec time desc')
36
            ->addOption('clean', 'c', InputOption::VALUE_REQUIRED, 'Command to be clean crontab data (status, last exec)')
37
            ->addOption('all', 'a', InputOption::VALUE_NONE, 'Command to be clean all crontab data (status, last exec)');
38
    }
39
40
    protected function execute(InputInterface $input, OutputInterface $output): int
41
    {
42
        set_time_limit(EnvHelper::getCrontabTimeout());
43
44
        $logger = EnvHelper::getLogger('bx_cron');
45
        if($logger) {
46
            $this->setLogger($logger);
47
        }
48
49
        $showStatus = $input->getOption('status');
50
        $byTime = $input->getOption('bytime');
51
        if($showStatus) {
52
            $sort = ($byTime ? self::SORT_TIME : self::SORT_NAME);
53
            $this->showStatus($output, $sort);
54
            return 0;
55
        }
56
57
        if(EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF)) {
58
            if($this->logger) {
59
                $this->logger->alert('BxCron switch off');
60
            }
61
            return 0;
62
        }
63
64
        if(!$this->lock()) {
65
            $msg = 'The command is already running in another process.';
66
            $output->writeln($msg);
67
            if($this->logger) {
68
                $this->logger->warning($msg);
69
            }
70
            return 0;
71
        }
72
73
        if($sleepInterval = EnvHelper::checkSleepInterval()) {
74
            $msg = sprintf("Sleep in interval %s", $sleepInterval);
75
            $output->writeln($msg);
76
            if($this->logger) {
77
                $this->logger->warning($msg);
78
            }
79
            return 0;
80
        }
81
82
        $clean = $input->getOption('clean');
83
        if($clean) {
84
            $command = $this->getApplication()->find($clean);
85
            $this->cleanJob($command->getName());
86
            $output->writeln($command->getName() . " will be executed now");
87
            return 0;
88
        }
89
90
        $cleanAll = $input->getOption('all');
91
        if($cleanAll) {
92
            $this->cleanJob();
93
            $output->writeln("All commands will be executed now");
94
            return 0;
95
        }
96
97
        $this->executeJobs($output);
98
99
        $this->release();
100
101
        return 0;
102
    }
103
104
    protected function showStatus(OutputInterface $output, $sort) {
105
106
        $table = new Table($output);
107
        $table->setStyle('box-double');
108
109
        $isSwitchOff = EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF);
110
111
        $jobs = $this->getCronJobs();
112
        $this->sortCronTab($jobs, $sort);
113
        $lastExec = 0;
114
        $hasError = false;
115
116
        foreach($jobs as $cmd => $job) {
117
            $execTime = $job['last_exec'];
118
            if($execTime > $lastExec) $lastExec = $execTime;
119
            if(!empty($job['error'])) {
120
                $hasError = true;
121
            }
122
        }
123
124
        $headStr = sprintf(
125
            "BX_CRONTAB_RUN: %s;  LAST_EXEC: %s;  AGENTS_COUNT: %d",
126
            ($isSwitchOff ? 'OFF' : 'ON'),
127
            ($lastExec ? date("d.m.Y H:i:s", $lastExec) : 'NONE'),
128
            count($jobs),
129
        );
130
131
        $header = [
132
            'Command',
133
            'Period',
134
            'Last Exec',
135
            'Status',
136
        ];
137
138
        if($hasError) {
139
            $header[] = 'Error';
140
        }
141
142
        $table->setHeaders([
143
            [new TableCell($headStr, ['colspan' => ($hasError ? 5 : 4)])],
144
            $header,
145
        ]);
146
147
        $cnt = 1;
148
        foreach($jobs as $cmd => $job) {
149
            if($cnt > 1) $table->addRow(new TableSeparator());
150
            $row = [
151
                $cmd,
152
                $job['period'],
153
                ($job['last_exec'] ? date("d.m.Y H:i:s", $job['last_exec']) : 'NONE'),
154
                $job['status'],
155
            ];
156
            if($hasError) {
157
                $row[] = $job['error'];
158
            }
159
            $table->addRow($row);
160
            $cnt++;
161
        }
162
163
        $table->render();
164
    }
165
166
    protected function cleanJob($command = false) {
167
168
        $crontab = [];
169
170
        if($command) {
171
            $crontab = $this->getCronTab();
172
            if($crontab === false) {
0 ignored issues
show
introduced by
The condition $crontab === false is always true.
Loading history...
173
                return false;
174
            }
175
            unset($crontab[$command]);
176
        }
177
178
        $this->setCronTab($crontab);
179
    }
180
181
    protected function executeJobs(OutputInterface $output) {
182
183
        $jobs = $this->getCronJobs();
184
        $allTimeout = EnvHelper::getCrontabTimeout();
185
        $workTime = 0;
186
187
        if(!empty($jobs)) {
188
189
            /*
190
             * Минимально допустимый период выполнения одной задачи
191
             * при котором гарантируется выполнение всех задач
192
             */
193
            $this->minAgentPeriod = (count($jobs) + 1) * EnvHelper::getBxCrontabPeriod();
194
            $msg = sprintf("Minimal agent period: %d", $this->minAgentPeriod);
195
            $this->logger->alert($msg);
0 ignored issues
show
Bug introduced by
The method alert() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

195
            $this->logger->/** @scrutinizer ignore-call */ 
196
                           alert($msg);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
196
            $output->writeln($msg);
197
198
            foreach($jobs as $cmd => $job) {
199
200
                $job['cmd'] = $cmd;
201
                if($this->isActualJob($job)) {
202
203
                    $job['status'] = self::EXEC_STATUS_WORK;
204
                    $job['start_time'] = time();
205
                    $this->updateJob($cmd, $job);
206
207
                    $command = $this->getApplication()->find($cmd);
208
                    $cmdInput = new ArrayInput(['command' => $cmd]);
209
                    $timeStart = microtime(true);
210
                    $execTime = 0;
211
                    try {
212
213
                        $returnCode = $command->run($cmdInput, $output);
214
                        $execTime = microtime(true) - $timeStart;
215
216
                        if(!$returnCode) {
217
218
                            $job['status'] = self::EXEC_STATUS_SUCCESS;
219
220
                            $msg = sprintf("%s: SUCCESS [%.2f s]", $cmd, $execTime);
221
                            if($this->logger) {
222
                                $this->logger->alert($msg);
223
                            }
224
                            $output->writeln(PHP_EOL . $msg);
225
226
                        } else {
227
228
                            $job['status'] = self::EXEC_STATUS_ERROR;
229
                            $job['error_code'] = $returnCode;
230
231
                            $msg = sprintf("%s: ERROR [%.2f s]", $cmd, $execTime);
232
                            if($this->logger) {
233
                                $this->logger->alert($msg);
234
                            }
235
                            $output->writeln(PHP_EOL . $msg);
236
                        }
237
238
                    } catch (\Exception $e) {
239
240
                        $job['status'] = self::EXEC_STATUS_ERROR;
241
                        $job['error'] = $e->getMessage();
242
243
244
                        if($this->logger) {
245
                            $this->logger->error($e, ['command' => $cmd]);
246
                        }
247
                        $output->writeln(PHP_EOL . 'ERROR: ' . $e->getMessage());
248
249
                    } finally {
250
251
                        if(!$execTime) {
252
                            $execTime = microtime(true) - $timeStart;
253
                        }
254
                        $job['last_exec'] = time();
255
                        $job['exec_time'] = round($execTime, 1);
256
                    }
257
258
                    $this->updateJob($cmd, $job);
259
260
                    $workTime += $execTime;
261
                    if($workTime * 2 > $allTimeout) {
262
                        break;
263
                    }
264
                    /*
265
                     * Let's do just one task
266
                     */
267
                    //break;
268
                }
269
            } // foreach($jobs as $cmd => $job)
270
        } // if(!empty($jobs))
271
    }
272
273
    protected function isActualJob(&$job): bool
274
    {
275
        $actual = false;
276
277
        if($job['status'] == self::EXEC_STATUS_WORK) {
278
            if($job['start_time'] && $job['start_time'] < (time() - self::RESTART_TIME)) {
279
                $actual = true;
280
            }
281
        }
282
283
        $period = intval($job['period']);
284
285
        if($period > 0) {
286
            if($period < $this->minAgentPeriod) {
287
                $job['orig_period'] = $period;
288
                $period = $job['period'] = $this->minAgentPeriod;
289
            }
290
            if(time() - $job['last_exec'] >= $period) {
291
                $actual = true;
292
            }
293
        } else if(!empty($job['times'])) {
294
            //TODO:
295
        }
296
297
        if($actual && !empty($job['interval'])) {
298
            $times = explode('-', $job['interval']);
299
            if(count($times) == 2) {
300
                $minTime = Time24::validateTimeString($times[0]);
301
                $maxTime = Time24::validateTimeString($times[1]);
302
                if($minTime && $maxTime) {
303
                    if(!Time24::inInterval($minTime, $maxTime)) {
304
                        $msg = sprintf("%s not in interval %s", $job['cmd'], $job['interval']);
305
                        if($this->logger) {
306
                            $this->logger->alert($msg);
307
                        }
308
                        return false;
309
                    }
310
                }
311
            }
312
        }
313
        return $actual;
314
    }
315
316
    public function getCronJobs(): array
317
    {
318
        $crontab = $this->getCronTab();
319
        if($crontab === false) {
0 ignored issues
show
introduced by
The condition $crontab === false is always true.
Loading history...
320
            return [];
321
        }
322
323
        /** @var Application $app */
324
        $app = $this->getApplication();
325
326
        $commands = $app->all();
327
328
        $selfCommands = [];
329
        foreach($commands as $command) {
330
            /** @var BxCommand $command */
331
            if($command instanceof BxCommand) {
332
                $name = $command->getName();
333
                $selfCommands[$name] = [
334
                    'object' => $command,
335
                ];
336
            }
337
        }
338
339
        $agents = [];
340
        $reader = new AnnotationReader();
341
        foreach($selfCommands as $cmd => $selfCommand) {
342
            $reflectionClass = new \ReflectionClass($selfCommand['object']);
343
            $annotations = $reader->getClassAnnotations($reflectionClass);
344
345
            foreach($annotations as $annotation) {
346
                if($annotation instanceof Agent) {
347
                    $agents[$cmd] = $annotation->toArray();
348
                }
349
            }
350
        }
351
352
        foreach($crontab as $cmd => $job) {
353
            if(is_array($job) && isset($agents[$cmd])) {
354
                $agents[$cmd] = array_merge($job, $agents[$cmd]);
355
            }
356
        }
357
358
        $this->setCronTab($agents);
359
360
        return $agents;
361
    }
362
363
    protected function updateJob($cmd, $job) {
364
365
        return $this->updateCronTab([$cmd => $job]);
366
    }
367
368
    protected function updateCronTab(array $changedAgents) {
369
370
        $crontab = $this->getCronTab();
371
372
        if($crontab === false) {
0 ignored issues
show
introduced by
The condition $crontab === false is always true.
Loading history...
373
            return false;
374
        } else {
375
            $crontab = array_merge($crontab, $changedAgents);
376
            return $this->setCronTab($crontab);
377
        }
378
    }
379
380
    protected function setCronTab(array $agents) {
381
382
        $isSuccess = true;
383
        $this->sortCronTab($agents);
384
385
        $filename = EnvHelper::getCrontabFile();
386
387
        $fh = fopen($filename, 'c');
388
        if (flock($fh, LOCK_EX)) {
389
            ftruncate($fh, 0);
390
            if(!fwrite($fh, json_encode($agents, JSON_PRETTY_PRINT))) {
391
                throw new \Exception('Unable to write BX_CRONTAB : ' . $filename);
392
            }
393
        } else {
394
            $isSuccess = false;
395
        }
396
        flock($fh, LOCK_UN);
397
        fclose($fh);
398
399
        return $isSuccess;
400
    }
401
402
    /**
403
     * @return array|false|mixed
404
     */
405
    protected function getCronTab() {
406
407
        $filename = EnvHelper::getCrontabFile();
408
409
        $fh = fopen($filename, 'r');
410
        if(!$fh) {
0 ignored issues
show
introduced by
$fh is of type resource, thus it always evaluated to false.
Loading history...
411
            return false;
412
        }
413
        if(flock($fh, LOCK_SH)) {
414
            $cronTab = [];
415
            clearstatcache();
416
            $filesize = (int) filesize($filename);
417
            if($filesize > 0 && $data = fread($fh, $filesize)) {
418
                $decoded = json_decode($data, true);
419
                if(is_array($decoded)) {
420
                    $cronTab = $decoded;
421
                } else {
422
                    throw new \Exception("Unable to parse cronTab");
423
                }
424
            }
425
        } else {
426
            $cronTab = false;
427
        }
428
        flock($fh, LOCK_UN);
429
        fclose($fh);
430
431
        return $cronTab;
432
    }
433
434
    protected function sortCronTab(array &$crontab, $sort = self::SORT_NAME) {
435
436
        if($sort == self::SORT_TIME) {
437
            $sorting = [];
438
            foreach($crontab as $cmd => $data) {
439
                $sorting[$cmd] = $data['last_exec'];
440
            }
441
            arsort($sorting, SORT_NUMERIC);
442
            $sorted = [];
443
            foreach($sorting as $cmd => $time) {
444
                $sorted[$cmd] = $crontab[$cmd];
445
            }
446
            $crontab = $sorted;
447
        } else {
448
            ksort($crontab, SORT_STRING);
449
        }
450
    }
451
}