Passed
Push — master ( e49e3e...821da0 )
by Ioannes
01:42
created

Cron::showStatus()   C

Complexity

Conditions 12
Paths 90

Size

Total Lines 60
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 40
c 1
b 0
f 0
dl 0
loc 60
rs 6.9666
cc 12
nc 90
nop 2

How to fix   Long Method    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 SORT_NAME = 'name';
24
    const SORT_TIME = 'time';
25
26
    private $minAgentPeriod;
27
28
    protected function configure() {
29
30
        $this->setName('system:cron')
31
            ->setDescription('Job sheduler for application comands')
32
            ->addOption('status', 's', InputOption::VALUE_NONE, 'Show BX_CRONTAB status table')
33
            ->addOption('bytime', 't', InputOption::VALUE_NONE, 'Sort status table by exec time desc')
34
            ->addOption('clean', 'c', InputOption::VALUE_REQUIRED, 'Command to be clean crontab data (status, last exec)');
35
    }
36
37
    protected function execute(InputInterface $input, OutputInterface $output)
38
    {
39
        $logger = EnvHelper::getLogger('bx_cron');
40
        if($logger) {
41
            $this->setLogger($logger);
42
        }
43
44
        $showStatus = $input->getOption('status');
45
        $byTime = $input->getOption('bytime');
46
        if($showStatus) {
47
            $sort = ($byTime ? self::SORT_TIME : self::SORT_NAME);
48
            $this->showStatus($output, $sort);
49
            return 0;
50
        }
51
52
        if(EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF)) {
53
            if($this->logger) {
54
                $this->logger->alert('BxCron switch off');
55
            }
56
            return 0;
57
        }
58
59
        if(!$this->lock()) {
60
            $msg = 'The command is already running in another process.';
61
            $output->writeln($msg);
62
            if($this->logger) {
63
                $this->logger->warning($msg);
64
            }
65
            return 0;
66
        }
67
68
        if($sleepInterval = EnvHelper::checkSleepInterval()) {
69
            $msg = sprintf("Sleep in interval %s", $sleepInterval);
70
            $output->writeln($msg);
71
            if($this->logger) {
72
                $this->logger->warning($msg);
73
            }
74
            return 0;
75
        }
76
77
        $clean = $input->getOption('clean');
78
        if($clean) {
79
            $command = $this->getApplication()->find($clean);
80
            $this->cleanJob($command->getName());
81
            $output->writeln($command->getName() . " will be executed now");
82
            return 0;
83
        }
84
85
        $this->executeJobs($output);
86
87
        $this->release();
88
    }
89
90
    protected function showStatus(OutputInterface $output, $sort) {
91
92
        $table = new Table($output);
93
        $table->setStyle('box-double');
94
95
        $isSwitchOff = EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF);
96
97
        $jobs = $this->getCronJobs();
98
        $this->sortCronTab($jobs, $sort);
99
        $lastExec = 0;
100
        $hasError = false;
101
102
        foreach($jobs as $cmd => $job) {
103
            $execTime = $job['last_exec'];
104
            if($execTime > $lastExec) $lastExec = $execTime;
105
            if(!empty($job['error'])) {
106
                $hasError = true;
107
            }
108
        }
109
110
        $headStr = sprintf(
111
            "BX_CRONTAB_RUN: %s;  LAST_EXEC: %s;  AGENTS_COUNT: %d",
112
            ($isSwitchOff ? 'OFF' : 'ON'),
113
            ($lastExec ? date("d.m.Y H:i:s", $lastExec) : 'NONE'),
114
            count($jobs),
115
        );
116
117
        $header = [
118
            'Command',
119
            'Period',
120
            'Last Exec',
121
            'Status',
122
        ];
123
124
        if($hasError) {
125
            $header[] = 'Error';
126
        }
127
128
        $table->setHeaders([
129
            [new TableCell($headStr, ['colspan' => ($hasError ? 5 : 4)])],
130
            $header,
131
        ]);
132
133
        $cnt = 1;
134
        foreach($jobs as $cmd => $job) {
135
            if($cnt > 1) $table->addRow(new TableSeparator());
136
            $row = [
137
                $cmd,
138
                $job['period'],
139
                ($job['last_exec'] ? date("d.m.Y H:i:s", $job['last_exec']) : 'NONE'),
140
                $job['status'],
141
            ];
142
            if($hasError) {
143
                $row[] = $job['error'];
144
            }
145
            $table->addRow($row);
146
            $cnt++;
147
        }
148
149
        $table->render();
150
    }
151
152
    protected function cleanJob($command) {
153
154
        $crontab = $this->getCronTab();
155
156
        unset($crontab[$command]);
157
158
        $this->setCronTab($crontab);
159
    }
160
161
    protected function executeJobs(OutputInterface $output) {
162
163
        $jobs = $this->getCronJobs();
164
165
        /*
166
         * Минимально допустимый период выполнения одной задачи
167
         * при котором гарантируется выполнение всех задач
168
         */
169
        $this->minAgentPeriod = (count($jobs) + 1) * EnvHelper::getBxCrontabPeriod();
170
171
        if(!empty($jobs)) {
172
173
            foreach($jobs as $cmd => $job) {
174
175
                if($this->isActualJob($job)) {
176
177
                    $job['status'] = self::EXEC_STATUS_WORK;
178
                    $this->updaateJob($cmd, $job);
179
180
                    $command = $this->getApplication()->find($cmd);
181
                    $cmdInput = new ArrayInput(['command' => $cmd]);
182
                    try {
183
184
                        $timeStart = microtime(true);
185
                        $returnCode = $command->run($cmdInput, $output);
186
187
                        if(!$returnCode) {
188
189
                            $job['status'] = self::EXEC_STATUS_SUCCESS;
190
191
                            $msg = sprintf("%s: SUCCESS [%.2f s]", $cmd, microtime(true) - $timeStart);
192
                            if($this->logger) {
193
                                $this->logger->alert($msg);
194
                            }
195
                            $output->writeln(PHP_EOL . $msg);
196
197
                        } else {
198
199
                            $job['status'] = self::EXEC_STATUS_ERROR;
200
                            $job['error_code'] = $returnCode;
201
202
                            $msg = sprintf("%s: ERROR [%.2f s]", $cmd, microtime(true) - $timeStart);
203
                            if($this->logger) {
204
                                $this->logger->alert($msg);
205
                            }
206
                            $output->writeln(PHP_EOL . $msg);
207
                        }
208
209
                    } catch (\Exception $e) {
210
211
                        $job['status'] = self::EXEC_STATUS_ERROR;
212
                        $job['error'] = $e->getMessage();
213
214
215
                        if($this->logger) {
216
                            $this->logger->error($e, ['command' => $cmd]);
217
                        }
218
                        $output->writeln(PHP_EOL . 'ERROR: ' . $e->getMessage());
219
220
                    } finally {
221
222
                        $job['last_exec'] = time();
223
                    }
224
225
                    $this->updaateJob($cmd, $job);
226
                    /*
227
                     * Let's do just one task
228
                     */
229
                    break;
230
                }
231
            } // foreach($jobs as $cmd => $job)
232
        } // if(!empty($jobs))
233
    }
234
235
    protected function isActualJob(&$job) {
236
237
        if(isset($job['status']) && $job['status'] !== self::EXEC_STATUS_SUCCESS) {
238
            return false;
239
        }
240
241
        $period = intval($job['period']);
242
243
        if($period > 0) {
244
            if($period < $this->minAgentPeriod) {
245
                $job['orig_period'] = $period;
246
                $period = $job['period'] = $this->minAgentPeriod;
247
            }
248
            if(time() - $job['last_exec'] >= $period) {
249
                return true;
250
            }
251
        } else if(!empty($job['times'])) {
252
            //TODO:
253
        }
254
255
        return false;
256
    }
257
258
    protected function getCronJobs() {
259
260
        /** @var Application $app */
261
        $app = $this->getApplication();
262
263
        $commands = $app->all();
264
265
        $selfCommands = [];
266
        foreach($commands as $command) {
267
            /** @var BxCommand $command */
268
            if($command instanceof BxCommand) {
269
                $name = $command->getName();
270
                $selfCommands[$name] = [
271
                    'object' => $command,
272
                ];
273
            }
274
        }
275
276
        $agents = [];
277
        $reader = new AnnotationReader();
278
        foreach($selfCommands as $cmd => $selfCommand) {
279
            $reflectionClass = new \ReflectionClass($selfCommand['object']);
280
            $annotations = $reader->getClassAnnotations($reflectionClass);
281
282
            foreach($annotations as $annotation) {
283
                if($annotation instanceof Agent) {
284
                    $agents[$cmd] = $annotation->toArray();
285
                }
286
            }
287
        }
288
289
        $crontab = $this->getCronTab();
290
291
        foreach($crontab as $cmd => $job) {
292
            if(is_array($job) && isset($agents[$cmd])) {
293
                $agents[$cmd] = array_merge($job, $agents[$cmd]);
294
            }
295
        }
296
297
        $this->setCronTab($agents);
298
299
        return $agents;
300
    }
301
302
    protected function updaateJob($cmd, $job) {
303
304
        $this->updateCronTab([$cmd => $job]);
305
    }
306
307
    protected function updateCronTab(array $changedAgents) {
308
309
        $crontab = $this->getCronTab();
310
311
        $crontab = array_merge($crontab, $changedAgents);
312
313
        $this->setCronTab($crontab);
314
    }
315
316
317
    protected function setCronTab(array $agents) {
318
319
        $this->sortCronTab($agents);
320
321
        $filename = EnvHelper::getCrontabFile();
322
323
        $fh = fopen($filename, 'c');
324
        if (flock($fh, LOCK_EX)) {
325
            ftruncate($fh, 0);
326
            if(!fwrite($fh, json_encode($agents, JSON_PRETTY_PRINT))) {
327
                throw new \Exception('Unable to write BX_CRONTAB : ' . $filename);
328
            }
329
        }
330
        flock($fh, LOCK_UN);
331
        fclose($fh);
332
    }
333
334
    /**
335
     * @return array
336
     */
337
    protected function getCronTab() {
338
339
        $cronTab = [];
340
341
        $filename = EnvHelper::getCrontabFile();
342
343
        $fh = fopen($filename, 'r');
344
        if(flock($fh, LOCK_SH)) {
345
            if($data = @fread($fh, filesize($filename))) {
346
                $decoded = json_decode($data, true);
347
                if(is_array($decoded)) {
348
                    $cronTab = $decoded;
349
                }
350
            }
351
        }
352
        flock($fh, LOCK_UN);
353
        fclose($fh);
354
355
        return $cronTab;
356
    }
357
358
    protected function sortCronTab(array &$crontab, $sort = self::SORT_NAME) {
359
360
        if($sort == self::SORT_TIME) {
361
            $sorting = [];
362
            foreach($crontab as $cmd => $data) {
363
                $sorting[$cmd] = $data['last_exec'];
364
            }
365
            arsort($sorting, SORT_NUMERIC);
366
            $sorted = [];
367
            foreach($sorting as $cmd => $time) {
368
                $sorted[$cmd] = $crontab[$cmd];
369
            }
370
            $crontab = $sorted;
371
        } else {
372
            ksort($crontab, SORT_STRING);
373
        }
374
    }
375
}