Passed
Push — master ( dc2440...49787f )
by Ioannes
07:37
created

Cron::execute()   C

Complexity

Conditions 12
Paths 22

Size

Total Lines 60
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

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