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