Passed
Push — master ( 2ef181...6dab9e )
by Ioannes
01:42
created

Cron::updateCronTab()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 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
    /**
18
     * Глобальный таймаут запуска процесса.
19
     * Устанавливает TTL блокировки
20
     */
21
    const EXEC_TIMEOUT = 600;
22
23
    /**
24
     * Период запуска задач кроном
25
     */
26
    const BX_CRON_PERIOD = 60;
27
28
    private $minAgentPeriod = self::BX_CRON_PERIOD;
29
30
    protected function configure() {
31
32
        $this->setName('system:cron')
33
            ->setDescription('Job sheduler for application comands');
34
    }
35
36
    protected function execute(InputInterface $input, OutputInterface $output)
37
    {
38
        $logger = EnvHelper::getLogger('bx_cron');
39
        if($logger) {
40
            $this->setLogger($logger);
41
        }
42
43
        $jobs = $this->getCronJobs();
44
45
        /*
46
         * Минимально допустимый период выполнения одной задачи
47
         * при котором гарантируется выполнение всех задач
48
         */
49
        $this->minAgentPeriod = (count($jobs) + 1) * self::BX_CRON_PERIOD;
50
51
        $workedJobs = [];
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
                        $workedJobs[$cmd] = $job;
69
70
                        $command = $this->getApplication()->find($cmd);
71
                        $cmdInput = new ArrayInput(['command' => $cmd]);
72
                        try {
73
74
                            $timeStart = microtime(true);
75
                            $returnCode = $command->run($cmdInput, $output);
76
77
                            if(!$returnCode) {
78
79
                                $workedJobs[$cmd]['status'] = self::EXEC_STATUS_SUCCESS;
80
                                $msg = sprintf("%s: SUCCESS [%.2f s]", $cmd, microtime(true) - $timeStart);
81
                                if($this->logger) {
82
                                    $this->logger->alert($msg);
83
                                }
84
                                $output->writeln(PHP_EOL . $msg);
85
86
                            } else {
87
88
                                $workedJobs[$cmd]['status'] = self::EXEC_STATUS_ERROR;
89
                                $workedJobs[$cmd]['error_code'] = $returnCode;
90
                                $msg = sprintf("%s: ERROR [%.2f s]", $cmd, microtime(true) - $timeStart);
91
                                if($this->logger) {
92
                                    $this->logger->alert($msg);
93
                                }
94
                                $output->writeln(PHP_EOL . $msg);
95
                            }
96
97
                        } catch (\Exception $e) {
98
99
                            $workedJobs[$cmd]['status'] = self::EXEC_STATUS_ERROR;
100
                            $workedJobs[$cmd]['error'] = $e->getMessage();
101
                            if($this->logger) {
102
                                $this->logger->error($e, ['command' => $cmd]);
103
                            }
104
                            $output->writeln(PHP_EOL . 'ERROR: ' . $e->getMessage());
105
106
                        } finally {
107
108
                            $workedJobs[$cmd]['last_exec'] = time();
109
                            $humanDate = new DateTime();
110
                            $workedJobs[$cmd]['last_date_time'] = $humanDate->toString();
111
                            $lock->release();
112
                        }
113
114
                        /*
115
                         * Выполняем только одну задачу
116
                         */
117
                        break;
118
119
                    } else {
120
                        if($this->logger) {
121
                            $this->logger->warning($cmd . " is locked");
122
                        }
123
                    }
124
                }
125
            }
126
        }
127
128
        $this->updateCronTab($workedJobs);
129
    }
130
131
    protected function getLockName($cmd) {
132
133
        return preg_replace('/[^a-z\d ]/i', '_', $cmd);
134
    }
135
136
    protected function isActualJob(&$job) {
137
138
        $period = intval($job['period']);
139
140
        if($period > 0) {
141
            if($period < $this->minAgentPeriod) {
142
                $job['orig_period'] = $period;
143
                $period = $job['period'] = $this->minAgentPeriod;
144
            }
145
            if(time() - $job['last_exec'] >= $period) {
146
                return true;
147
            }
148
        } else if(!empty($job['times'])) {
149
            //TODO:
150
        }
151
152
        return false;
153
    }
154
155
    protected function getCronJobs() {
156
157
        /** @var Application $app */
158
        $app = $this->getApplication();
159
160
        $commands = $app->all();
161
162
        $selfCommands = [];
163
        foreach($commands as $command) {
164
            /** @var BxCommand $command */
165
            if($command instanceof BxCommand) {
166
                $name = $command->getName();
167
                $selfCommands[$name] = [
168
                    'object' => $command,
169
                ];
170
            }
171
        }
172
173
        $agents = [];
174
        $reader = new AnnotationReader();
175
        foreach($selfCommands as $cmd => $selfCommand) {
176
            $reflectionClass = new \ReflectionClass($selfCommand['object']);
177
            $annotations = $reader->getClassAnnotations($reflectionClass);
178
179
            foreach($annotations as $annotation) {
180
                if($annotation instanceof Agent) {
181
                    $agents[$cmd] = $annotation->toArray();
182
                }
183
            }
184
        }
185
186
        $crontab = $this->getCronTab();
187
188
        if(is_array($crontab)) {
189
            foreach($crontab as $cmd => $job) {
190
                if(is_array($job) && isset($agents[$cmd])) {
191
                    $agents[$cmd] = array_merge($job, $agents[$cmd]);
192
                }
193
            }
194
        } else {
195
            $this->setCronTab($agents);
196
        }
197
198
        return $agents;
199
    }
200
201
    protected function updateCronTab($changedAgents) {
202
203
        $crontab = $this->getCronTab();
204
205
        $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

205
        $crontab = array_merge(/** @scrutinizer ignore-type */ $crontab, $changedAgents);
Loading history...
206
207
        $this->setCronTab($crontab);
208
    }
209
210
211
    protected function setCronTab(array $agents) {
212
213
        $filename = EnvHelper::getCrontabFile();
214
215
        $fh = fopen($filename, 'c');
216
        if (flock($fh, LOCK_EX)) {
217
            ftruncate($fh, 0);
218
            if(!fwrite($fh, json_encode($agents, JSON_PRETTY_PRINT))) {
219
                throw new \Exception('Unable to write BX_CRONTAB : ' . $filename);
220
            }
221
        }
222
        flock($fh, LOCK_UN);
223
        fclose($fh);
224
    }
225
226
    /**
227
     * @return mixed|null
228
     */
229
    protected function getCronTab() {
230
231
        $cronTab = null;
232
233
        $filename = EnvHelper::getCrontabFile();
234
235
        $fh = fopen($filename, 'r');
236
        if(flock($fh, LOCK_SH)) {
237
            $data = @fread($fh, filesize($filename));
238
            $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

238
            $cronTab = json_decode(/** @scrutinizer ignore-type */ $data, true);
Loading history...
239
        }
240
        flock($fh, LOCK_UN);
241
        fclose($fh);
242
243
        return $cronTab;
244
    }
245
}