Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.67% covered (warning)
79.67%
145 / 182
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
OnContinuouslyCommand
79.67% covered (warning)
79.67%
145 / 182
33.33% covered (danger)
33.33%
2 / 6
20.72
0.00% covered (danger)
0.00%
0 / 1
 getAllowedAppEnvs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
74.40% covered (warning)
74.40%
93 / 125
0.00% covered (danger)
0.00%
0 / 1
2.07
 isDeploying
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 daily
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
8.04
 every
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
4.03
 getTimeOnlyDiffSeconds
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Olz\Command;
4
5use Olz\Command\Common\OlzCommand;
6use Olz\Entity\Throttling;
7use Symfony\Component\Console\Attribute\AsCommand;
8use Symfony\Component\Console\Command\Command;
9use Symfony\Component\Console\Input\ArrayInput;
10use Symfony\Component\Console\Input\InputInterface;
11use Symfony\Component\Console\Output\OutputInterface;
12
13#[AsCommand(name: 'olz:on-continuously')]
14class OnContinuouslyCommand extends OlzCommand {
15    /** @return array<string> */
16    protected function getAllowedAppEnvs(): array {
17        return ['dev', 'test', 'staging', 'prod'];
18    }
19
20    protected function handle(InputInterface $input, OutputInterface $output): int {
21        set_time_limit(4000);
22        ignore_user_abort(true);
23
24        $this->logAndOutput("Running continuously...", level: 'debug');
25        $throttling_repo = $this->entityManager()->getRepository(Throttling::class);
26        $throttling_repo->recordOccurrenceOf('on_continuously', $this->dateUtils()->getIsoNow());
27
28        if ($this->isDeploying()) {
29            $this->logAndOutput("Deploying. Cancel running continuously.", level: 'debug');
30            return Command::SUCCESS;
31        }
32
33        $this->logAndOutput("Continuously processing email...", level: 'debug');
34        $this->symfonyUtils()->callCommand(
35            'olz:process-email',
36            new ArrayInput([]),
37            $output,
38        );
39
40        $this->daily('01:00:00', 'clean-temp-directory', function () use ($output) {
41            $this->symfonyUtils()->callCommand(
42                'olz:clean-temp-directory',
43                new ArrayInput([]),
44                $output,
45            );
46        });
47        $this->daily('01:05:00', 'clean-temp-database', function () use ($output) {
48            $this->symfonyUtils()->callCommand(
49                'olz:clean-temp-database',
50                new ArrayInput([]),
51                $output,
52            );
53        });
54        $this->daily('01:10:00', 'clean-logs', function () use ($output) {
55            $this->symfonyUtils()->callCommand(
56                'olz:clean-logs',
57                new ArrayInput([]),
58                $output,
59            );
60        });
61        $this->daily('01:15:00', 'send-telegram-configuration', function () use ($output) {
62            $this->symfonyUtils()->callCommand(
63                'olz:send-telegram-configuration',
64                new ArrayInput([]),
65                $output,
66            );
67        });
68        $this->daily('01:20:00', 'sync-solv', function () use ($output) {
69            $this->symfonyUtils()->callCommand(
70                'olz:sync-solv',
71                new ArrayInput([]),
72                $output,
73            );
74        });
75
76        $this->daily('08:15:00', 'send-weekly-summary', function () use ($output) {
77            $this->symfonyUtils()->callCommand(
78                'olz:send-weekly-summary',
79                new ArrayInput([]),
80                $output,
81            );
82        });
83
84        $this->daily('14:30:00', 'send-monthly-preview', function () use ($output) {
85            $this->symfonyUtils()->callCommand(
86                'olz:send-monthly-preview',
87                new ArrayInput([]),
88                $output,
89            );
90        });
91
92        $this->daily('15:14:00', 'send-weekly-preview', function () use ($output) {
93            $this->symfonyUtils()->callCommand(
94                'olz:send-weekly-preview',
95                new ArrayInput([]),
96                $output,
97            );
98        });
99
100        $this->daily('16:27:00', 'send-deadline-warning', function () use ($output) {
101            $this->symfonyUtils()->callCommand(
102                'olz:send-deadline-warning',
103                new ArrayInput([]),
104                $output,
105            );
106        });
107
108        $this->daily('17:30:00', 'send-daily-summary', function () use ($output) {
109            $this->symfonyUtils()->callCommand(
110                'olz:send-daily-summary',
111                new ArrayInput([]),
112                $output,
113            );
114        });
115
116        $this->daily('18:30:00', 'send-reminders', function () use ($output) {
117            $this->symfonyUtils()->callCommand(
118                'olz:send-email-config-reminder',
119                new ArrayInput([]),
120                $output,
121            );
122            $this->symfonyUtils()->callCommand(
123                'olz:send-role-reminder',
124                new ArrayInput([]),
125                $output,
126            );
127            $this->symfonyUtils()->callCommand(
128                'olz:send-telegram-config-reminder',
129                new ArrayInput([]),
130                $output,
131            );
132        });
133
134        $this->every('10 minutes', 'sync-strava', function () use ($output) {
135            $this->symfonyUtils()->callCommand(
136                'olz:sync-strava',
137                new ArrayInput(['year' => '2025']),
138                $output,
139            );
140        });
141
142        $this->logAndOutput("Stopping workers...", level: 'debug');
143        $this->symfonyUtils()->callCommand(
144            'messenger:stop-workers',
145            new ArrayInput([]),
146            $output,
147        );
148        $this->logAndOutput("Consume messages...", level: 'debug');
149        $this->symfonyUtils()->callCommand(
150            'messenger:consume',
151            new ArrayInput([
152                'receivers' => ['async'],
153                '--no-reset' => '--no-reset',
154            ]),
155            $output,
156        );
157
158        $this->logAndOutput("Ran continuously.", level: 'debug');
159        return Command::SUCCESS;
160    }
161
162    protected function isDeploying(): bool {
163        $status = $this->envUtils()->getDeployStatus();
164        $minus_one_hour = \DateInterval::createFromDateString("-1 hours");
165        $one_hour_ago = (new \DateTime($this->dateUtils()->getIsoNow()))->add($minus_one_hour);
166        if ($status['date'] < $one_hour_ago->format('Y-m-d H:i:s')) {
167            return false;
168        }
169        return ($status['status'] ?? 'IDLE') !== 'IDLE';
170    }
171
172    /** @param callable(): void $fn */
173    public function daily(string $time, string $ident, callable $fn): void {
174        $throttling_repo = $this->entityManager()->getRepository(Throttling::class);
175        $last_occurrence = $throttling_repo->getLastOccurrenceOf($ident);
176        $now = new \DateTime($this->dateUtils()->getIsoNow());
177        $is_too_soon = false;
178        if ($last_occurrence) {
179            // Consider daylight saving change date => not 23 hours!
180            $min_interval = \DateInterval::createFromDateString('+22 hours');
181            $min_now = $last_occurrence->add($min_interval);
182            $is_too_soon = $now < $min_now;
183        }
184        $time_diff = $this->getTimeOnlyDiffSeconds($now->format('H:i:s'), $time);
185        $is_right_time_of_day = $time_diff >= 0 && $time_diff < 7200; // 2h window
186        $should_execute_now = !$is_too_soon && $is_right_time_of_day;
187        if ($should_execute_now) {
188            $throttling_repo->recordOccurrenceOf($ident, $this->dateUtils()->getIsoNow());
189            try {
190                $this->logAndOutput("Executing daily ({$time}{$ident}...", level: 'info');
191                $fn();
192            } catch (\Throwable $th) {
193                $this->logAndOutput("Daily ({$time}{$ident} failed", level: 'error');
194            }
195        } else {
196            $pretty_reasons = [];
197            if ($is_too_soon) {
198                $pretty_reasons[] = 'too soon';
199            }
200            if (!$is_right_time_of_day) {
201                $pretty_reasons[] = "not the right time (diff: {$time_diff})";
202            }
203            $pretty_reason = implode(", ", $pretty_reasons);
204            $this->log()->debug("Not executing daily ({$time}{$ident}{$pretty_reason}");
205        }
206    }
207
208    public function every(string $interval, string $ident, callable $fn): void {
209        $throttling_repo = $this->entityManager()->getRepository(Throttling::class);
210        $last_occurrence = $throttling_repo->getLastOccurrenceOf($ident);
211        $now = new \DateTime($this->dateUtils()->getIsoNow());
212        $is_too_soon = false;
213        if ($last_occurrence) {
214            $min_interval = \DateInterval::createFromDateString("+{$interval}");
215            $this->generalUtils()->checkNotFalse($min_interval, "Invalid interval: +{$interval}");
216            $min_now = $last_occurrence->add($min_interval);
217            $is_too_soon = $now < $min_now;
218        }
219        $should_execute_now = !$is_too_soon;
220        if ($should_execute_now) {
221            $throttling_repo->recordOccurrenceOf($ident, $this->dateUtils()->getIsoNow());
222            try {
223                $this->logAndOutput("Executing {$ident} (every {$interval})...", level: 'info');
224                $fn();
225            } catch (\Throwable $th) {
226                $this->logAndOutput("Executing {$ident} (every {$interval}) failed", level: 'error');
227            }
228        } else {
229            $this->log()->debug("Not executing {$ident} (every {$interval}): too soon");
230        }
231    }
232
233    public function getTimeOnlyDiffSeconds(string $iso_value, string $iso_cmp): int {
234        $value = new \DateTime($iso_value, new \DateTimeZone('UTC'));
235        $cmp = new \DateTime($iso_cmp, new \DateTimeZone('UTC'));
236
237        $seconds_diff = $value->getTimestamp() - $cmp->getTimestamp();
238        $time_only_diff = $seconds_diff % 86400;
239        return match (true) {
240            $time_only_diff < -43200 => $time_only_diff + 86400,
241            $time_only_diff >= 43200 => $time_only_diff - 86400,
242            default => $time_only_diff,
243        };
244    }
245}