cron
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-30 03:00:21 +02:00
parent 0c5c89acfa
commit 8879a4ae5c
5 changed files with 468 additions and 187 deletions

View File

@@ -17,7 +17,7 @@ final class ModuleCronScheduler
public function definitions(string $moduleName): array
{
$module = $this->modules->get($moduleName);
$tasks = is_array($module['cron_tasks'] ?? null) ? $module['cron_tasks'] : [];
$tasks = is_array($module['scheduler_jobs'] ?? null) ? $module['scheduler_jobs'] : (is_array($module['cron_tasks'] ?? null) ? $module['cron_tasks'] : []);
$definitions = [];
foreach ($tasks as $task) {
@@ -35,6 +35,7 @@ final class ModuleCronScheduler
'name' => $name,
'label' => trim((string) ($task['label'] ?? $name)),
'callback' => $callback,
'mode' => in_array((string) ($task['mode'] ?? 'single'), ['single', 'multi'], true) ? (string) ($task['mode'] ?? 'single') : 'single',
'default_enabled' => array_key_exists('default_enabled', $task) ? (bool) $task['default_enabled'] : false,
'default_cron' => trim((string) ($task['default_cron'] ?? '0 * * * *')),
'default_timezone' => trim((string) ($task['default_timezone'] ?? 'UTC')) ?: 'UTC',
@@ -61,45 +62,50 @@ final class ModuleCronScheduler
$statuses = [];
foreach ($definitions as $name => $definition) {
$state = $states[$name] ?? [];
$config = $this->jobConfig($definition, $settings);
$timezone = $this->safeTimezone((string) ($config['timezone'] ?? 'UTC'));
$expression = trim((string) ($config['cron_expression'] ?? ''));
$isLocked = $this->isLockActive($state, $nowUtc->getTimestamp());
$parseError = null;
$previousDueUtc = null;
$nextDueUtc = null;
$isDue = false;
foreach ($this->jobConfigs($definition, $settings) as $entryIndex => $config) {
$stateKey = $this->stateKey($name, $entryIndex);
$state = $states[$stateKey] ?? [];
$timezone = $this->safeTimezone((string) ($config['timezone'] ?? 'UTC'));
$expression = trim((string) ($config['cron_expression'] ?? ''));
$isLocked = $this->isLockActive($state, $nowUtc->getTimestamp());
$parseError = null;
$previousDueUtc = null;
$nextDueUtc = null;
$isDue = false;
try {
$cron = CronExpression::parse($expression);
$previousDueUtc = $cron->previousRun($nowUtc, $timezone);
$lastScheduledFor = $this->parseUtc((string) ($state['last_scheduled_for'] ?? ''));
$isDue = !empty($config['enabled'])
&& !$isLocked
&& $previousDueUtc instanceof DateTimeImmutable
&& ($lastScheduledFor === null || $previousDueUtc > $lastScheduledFor);
$nextDueUtc = $isDue
? $previousDueUtc
: $cron->nextRun($nowUtc, $timezone);
} catch (\Throwable $exception) {
$parseError = $exception->getMessage();
try {
$cron = CronExpression::parse($expression);
$previousDueUtc = $cron->previousRun($nowUtc, $timezone);
$lastScheduledFor = $this->parseUtc((string) ($state['last_scheduled_for'] ?? ''));
$isDue = !empty($config['enabled'])
&& !$isLocked
&& $previousDueUtc instanceof DateTimeImmutable
&& ($lastScheduledFor === null || $previousDueUtc > $lastScheduledFor);
$nextDueUtc = $isDue
? $previousDueUtc
: $cron->nextRun($nowUtc, $timezone);
} catch (\Throwable $exception) {
$parseError = $exception->getMessage();
}
$statuses[] = $definition + [
'job_name' => $name,
'entry_index' => $entryIndex,
'state_key' => $stateKey,
'config' => $config,
'state' => $state,
'enabled' => !empty($config['enabled']),
'cron_expression' => $expression,
'timezone' => $timezone->getName(),
'is_due' => $isDue,
'is_locked' => $isLocked,
'parse_error' => $parseError,
'previous_due_at' => $previousDueUtc?->format('Y-m-d H:i:s'),
'next_due_at' => $nextDueUtc?->format('Y-m-d H:i:s'),
'previous_due_at_local' => $previousDueUtc?->setTimezone($timezone)->format('Y-m-d H:i:s'),
'next_due_at_local' => $nextDueUtc?->setTimezone($timezone)->format('Y-m-d H:i:s'),
];
}
$statuses[] = $definition + [
'config' => $config,
'state' => $state,
'enabled' => !empty($config['enabled']),
'cron_expression' => $expression,
'timezone' => $timezone->getName(),
'is_due' => $isDue,
'is_locked' => $isLocked,
'parse_error' => $parseError,
'previous_due_at' => $previousDueUtc?->format('Y-m-d H:i:s'),
'next_due_at' => $nextDueUtc?->format('Y-m-d H:i:s'),
'previous_due_at_local' => $previousDueUtc?->setTimezone($timezone)->format('Y-m-d H:i:s'),
'next_due_at_local' => $nextDueUtc?->setTimezone($timezone)->format('Y-m-d H:i:s'),
];
}
return $statuses;
@@ -115,14 +121,15 @@ final class ModuleCronScheduler
if (!$this->modules->hasFunction($moduleName, (string) $task['callback'])) {
$results[] = [
'task' => $task['name'],
'task' => $task['job_name'],
'entry_index' => $task['entry_index'],
'ok' => false,
'message' => 'Callback nicht registriert.',
];
continue;
}
if (!$this->acquireLock($moduleName, (string) $task['name'], (int) $task['lock_minutes'])) {
if (!$this->acquireLock($moduleName, (string) $task['state_key'], (int) $task['lock_minutes'])) {
continue;
}
@@ -130,7 +137,7 @@ final class ModuleCronScheduler
$scheduledForUtc = trim((string) ($task['previous_due_at'] ?? ''));
$timezone = $this->safeTimezone((string) ($task['timezone'] ?? 'UTC'));
$this->persistState($moduleName, (string) $task['name'], [
$this->persistState($moduleName, (string) $task['state_key'], [
'last_started_at' => $startedAt,
'last_status' => 'running',
'last_message' => 'Cron-Lauf gestartet.',
@@ -163,16 +170,17 @@ final class ModuleCronScheduler
if ($ok && !$skipped) {
$payload['last_success_at'] = $finishedAt;
}
$this->persistState($moduleName, (string) $task['name'], $payload);
$this->persistState($moduleName, (string) $task['state_key'], $payload);
$results[] = [
'task' => $task['name'],
'task' => $task['job_name'],
'entry_index' => $task['entry_index'],
'ok' => $ok,
'message' => $message,
];
} catch (\Throwable $e) {
$finishedAt = gmdate('Y-m-d H:i:s');
$this->persistState($moduleName, (string) $task['name'], [
$this->persistState($moduleName, (string) $task['state_key'], [
'last_finished_at' => $finishedAt,
'last_status' => 'error',
'last_message' => $e->getMessage(),
@@ -180,7 +188,8 @@ final class ModuleCronScheduler
'last_scheduled_for' => $scheduledForUtc !== '' ? $scheduledForUtc : null,
]);
$results[] = [
'task' => $task['name'],
'task' => $task['job_name'],
'entry_index' => $task['entry_index'],
'ok' => false,
'message' => $e->getMessage(),
];
@@ -190,19 +199,64 @@ final class ModuleCronScheduler
return $results;
}
private function jobConfig(array $definition, array $settings): array
private function jobConfigs(array $definition, array $settings): array
{
$jobs = is_array($settings['cron_jobs'] ?? null) ? $settings['cron_jobs'] : [];
$job = is_array($jobs[$definition['name']] ?? null) ? $jobs[$definition['name']] : [];
$jobs = is_array($settings['scheduler_jobs'] ?? null)
? $settings['scheduler_jobs']
: (is_array($settings['cron_jobs'] ?? null) ? $settings['cron_jobs'] : []);
$jobExists = array_key_exists($definition['name'], $jobs) && is_array($jobs[$definition['name']] ?? null);
$job = $jobExists ? $jobs[$definition['name']] : [];
$timezoneSetting = trim((string) ($definition['timezone_setting'] ?? ''));
$fallbackTimezone = $timezoneSetting !== '' ? trim((string) ($settings[$timezoneSetting] ?? '')) : '';
return [
'enabled' => array_key_exists('enabled', $job) ? $this->settingBool($job['enabled'], (bool) $definition['default_enabled']) : (bool) $definition['default_enabled'],
'cron_expression' => trim((string) ($job['cron_expression'] ?? $definition['default_cron'] ?? '')),
'timezone' => trim((string) ($job['timezone'] ?? ($fallbackTimezone !== '' ? $fallbackTimezone : $definition['default_timezone']))),
'builder' => is_array($job['builder'] ?? null) ? $job['builder'] : [],
$defaultEntry = [
'enabled' => (bool) $definition['default_enabled'],
'cron_expression' => trim((string) ($definition['default_cron'] ?? '0 * * * *')),
'timezone' => trim((string) ($fallbackTimezone !== '' ? $fallbackTimezone : ($definition['default_timezone'] ?? 'UTC'))),
'builder' => [],
];
$entries = is_array($job['entries'] ?? null) ? $job['entries'] : [];
if (!$jobExists && $entries === []) {
$legacyEntry = [];
foreach (['enabled', 'cron_expression', 'timezone', 'builder'] as $field) {
if (array_key_exists($field, $job)) {
$legacyEntry[$field] = $job[$field];
}
}
$entries = [$legacyEntry !== [] ? $legacyEntry : $defaultEntry];
}
if ($jobExists && $entries === [] && ($definition['mode'] ?? 'single') === 'multi') {
return [];
}
$result = [];
foreach (array_values($entries) as $entry) {
if (!is_array($entry)) {
continue;
}
$result[] = [
'enabled' => array_key_exists('enabled', $entry) ? $this->settingBool($entry['enabled'], (bool) $defaultEntry['enabled']) : (bool) $defaultEntry['enabled'],
'cron_expression' => trim((string) ($entry['cron_expression'] ?? $defaultEntry['cron_expression'])),
'timezone' => trim((string) ($entry['timezone'] ?? $defaultEntry['timezone'])),
'builder' => is_array($entry['builder'] ?? null) ? $entry['builder'] : [],
];
}
if ($result === []) {
$result[] = $defaultEntry;
}
if (($definition['mode'] ?? 'single') !== 'multi') {
return [0 => $result[0]];
}
return $result;
}
private function stateKey(string $jobName, int $entryIndex): string
{
return $jobName . '#' . $entryIndex;
}
private function fetchStates(string $moduleName): array