Passed
Push — master ( a8eabf...729b2b )
by Ioannes
01:45
created

Cron   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 39
eloc 115
c 4
b 2
f 0
dl 0
loc 244
rs 9.28

9 Methods

Rating   Name   Duplication   Size   Complexity  
B isActualJob() 0 21 7
F execute() 0 90 13
A getCronTab() 0 15 2
A configure() 0 4 1
A getLockName() 0 3 1
A updateCronTab() 0 7 1
A setCronTab() 0 13 3
B getCronJobs() 0 44 10
A updaateJob() 0 3 1
1
<?php
2
namespace App\BxConsole;
3
4
use Bitrix\Main\Type\DateTime;
0 ignored issues
show
Bug introduced by
The type Bitrix\Main\Type\DateTime 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...
5
use Doctrine\Common\Annotations\AnnotationReader;
6
use Symfony\Component\Console\Input\ArrayInput;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use App\BxConsole\Annotations\Agent;
10
use Symfony\Component\Lock\LockFactory;
11
use Symfony\Component\Lock\Store\FlockStore;
12
13
class Cron extends BxCommand {
14
15
    const EXEC_STATUS_SUCCESS = 'SUCCESS';
16
    const EXEC_STATUS_ERROR = 'ERROR';
17
    const EXEC_STATUS_WORK = 'WORK';
18
19
    /**
20
     * Глобальный таймаут запуска процесса.
21
     * Устанавливает TTL блокировки
22
     */
23
    const EXEC_TIMEOUT = 600;
24
25
    /**
26
     * Период запуска задач кроном
27
     */
28
    const BX_CRON_PERIOD = 60;
29
30
    private $minAgentPeriod = self::BX_CRON_PERIOD;
31
32
    protected function configure() {
33
34
        $this->setName('system:cron')
35
            ->setDescription('Job sheduler for application comands');
36
    }
37
38
    protected function execute(InputInterface $input, OutputInterface $output)
39
    {
40
        $logger = EnvHelper::getLogger('bx_cron');
41
        if($logger) {
42
            $this->setLogger($logger);
43
        }
44
45
        $jobs = $this->getCronJobs();
46
47
        /*
48
         * Минимально допустимый период выполнения одной задачи
49
         * при котором гарантируется выполнение всех задач
50
         */
51
        $this->minAgentPeriod = (count($jobs) + 1) * self::BX_CRON_PERIOD;
52
53
        if(!empty($jobs)) {
54
55
            $lockStore = new FlockStore(pathinfo(EnvHelper::getCrontabFile(), PATHINFO_DIRNAME));
0 ignored issues
show
Bug introduced by
It seems like pathinfo(App\BxConsole\E...nsole\PATHINFO_DIRNAME) can also be of type array; however, parameter $lockPath of Symfony\Component\Lock\S...ockStore::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

55
            $lockStore = new FlockStore(/** @scrutinizer ignore-type */ pathinfo(EnvHelper::getCrontabFile(), PATHINFO_DIRNAME));
Loading history...
56
            $lockFactory = new LockFactory($lockStore);
57
            if($this->logger) {
58
                $lockFactory->setLogger($this->logger);
59
            }
60
61
            foreach($jobs as $cmd => $job) {
62
63
                if($this->isActualJob($job)) {
64
65
                    $lock = $lockFactory->createLock($this->getLockName($cmd), self::EXEC_TIMEOUT);
66
                    if($lock->acquire()) {
67
68
                        $job['status'] = self::EXEC_STATUS_WORK;
69
                        $this->updaateJob($cmd, $job);
70
71
                        $command = $this->getApplication()->find($cmd);
72
                        $cmdInput = new ArrayInput(['command' => $cmd]);
73
                        try {
74
75
                            $timeStart = microtime(true);
76
                            $returnCode = $command->run($cmdInput, $output);
77
78
                            if(!$returnCode) {
79
80
                                $job['status'] = self::EXEC_STATUS_SUCCESS;
81
82
                                $msg = sprintf("%s: SUCCESS [%.2f s]", $cmd, microtime(true) - $timeStart);
83
                                if($this->logger) {
84
                                    $this->logger->alert($msg);
85
                                }
86
                                $output->writeln(PHP_EOL . $msg);
87
88
                            } else {
89
90
                                $job['status'] = self::EXEC_STATUS_ERROR;
91
                                $job['error_code'] = $returnCode;
92
93
                                $msg = sprintf("%s: ERROR [%.2f s]", $cmd, microtime(true) - $timeStart);
94
                                if($this->logger) {
95
                                    $this->logger->alert($msg);
96
                                }
97
                                $output->writeln(PHP_EOL . $msg);
98
                            }
99
100
                        } catch (\Exception $e) {
101
102
                            $job['status'] = self::EXEC_STATUS_ERROR;
103
                            $job['error'] = $e->getMessage();
104
105
106
                            if($this->logger) {
107
                                $this->logger->error($e, ['command' => $cmd]);
108
                            }
109
                            $output->writeln(PHP_EOL . 'ERROR: ' . $e->getMessage());
110
111
                        } finally {
112
113
                            $job['last_exec'] = time();
114
                            $humanDate = new DateTime();
115
                            $job['last_date_time'] = $humanDate->toString();
116
                            $lock->release();
117
                        }
118
119
                        $this->updaateJob($cmd, $job);
120
                        /*
121
                         * Let's do just one task
122
                         */
123
                        break;
124
125
                    } else {
126
                        if($this->logger) {
127
                            $this->logger->warning($cmd . " is locked");
128
                        }
129
                    }
130
                }
131
            }
132
        }
133
    }
134
135
    protected function getLockName($cmd) {
136
137
        return preg_replace('/[^a-z\d ]/i', '_', $cmd);
138
    }
139
140
    protected function isActualJob(&$job) {
141
142
        if(isset($job['status']) && $job['status'] !== self::EXEC_STATUS_SUCCESS) {
143
            return false;
144
        }
145
146
        $period = intval($job['period']);
147
148
        if($period > 0) {
149
            if($period < $this->minAgentPeriod) {
150
                $job['orig_period'] = $period;
151
                $period = $job['period'] = $this->minAgentPeriod;
152
            }
153
            if(time() - $job['last_exec'] >= $period) {
154
                return true;
155
            }
156
        } else if(!empty($job['times'])) {
157
            //TODO:
158
        }
159
160
        return false;
161
    }
162
163
    protected function getCronJobs() {
164
165
        /** @var Application $app */
166
        $app = $this->getApplication();
167
168
        $commands = $app->all();
169
170
        $selfCommands = [];
171
        foreach($commands as $command) {
172
            /** @var BxCommand $command */
173
            if($command instanceof BxCommand) {
174
                $name = $command->getName();
175
                $selfCommands[$name] = [
176
                    'object' => $command,
177
                ];
178
            }
179
        }
180
181
        $agents = [];
182
        $reader = new AnnotationReader();
183
        foreach($selfCommands as $cmd => $selfCommand) {
184
            $reflectionClass = new \ReflectionClass($selfCommand['object']);
185
            $annotations = $reader->getClassAnnotations($reflectionClass);
186
187
            foreach($annotations as $annotation) {
188
                if($annotation instanceof Agent) {
189
                    $agents[$cmd] = $annotation->toArray();
190
                }
191
            }
192
        }
193
194
        $crontab = $this->getCronTab();
195
196
        if(is_array($crontab)) {
197
            foreach($crontab as $cmd => $job) {
198
                if(is_array($job) && isset($agents[$cmd])) {
199
                    $agents[$cmd] = array_merge($job, $agents[$cmd]);
200
                }
201
            }
202
        }
203
204
        $this->setCronTab($agents);
205
206
        return $agents;
207
    }
208
209
    protected function updaateJob($cmd, $job) {
210
211
        $this->updateCronTab([$cmd => $job]);
212
    }
213
214
    protected function updateCronTab(array $changedAgents) {
215
216
        $crontab = $this->getCronTab();
217
218
        $crontab = array_merge($crontab, $changedAgents);
0 ignored issues
show
Bug introduced by
It seems like $crontab can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

218
        $crontab = array_merge(/** @scrutinizer ignore-type */ $crontab, $changedAgents);
Loading history...
219
220
        $this->setCronTab($crontab);
221
    }
222
223
224
    protected function setCronTab(array $agents) {
225
226
        $filename = EnvHelper::getCrontabFile();
227
228
        $fh = fopen($filename, 'c');
229
        if (flock($fh, LOCK_EX)) {
230
            ftruncate($fh, 0);
231
            if(!fwrite($fh, json_encode($agents, JSON_PRETTY_PRINT))) {
232
                throw new \Exception('Unable to write BX_CRONTAB : ' . $filename);
233
            }
234
        }
235
        flock($fh, LOCK_UN);
236
        fclose($fh);
237
    }
238
239
    /**
240
     * @return mixed|null
241
     */
242
    protected function getCronTab() {
243
244
        $cronTab = null;
245
246
        $filename = EnvHelper::getCrontabFile();
247
248
        $fh = fopen($filename, 'r');
249
        if(flock($fh, LOCK_SH)) {
250
            $data = @fread($fh, filesize($filename));
251
            $cronTab = json_decode($data, true);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

251
            $cronTab = json_decode(/** @scrutinizer ignore-type */ $data, true);
Loading history...
252
        }
253
        flock($fh, LOCK_UN);
254
        fclose($fh);
255
256
        return $cronTab;
257
    }
258
}