Files
desktop/docs/Umsetzungsanweisung/Old-Nexus/src/App/ModuleTaskScheduler.php
Lars Gebhardt-Kusche 2cdd14c400
All checks were successful
Deploy / deploy-staging (push) Successful in 24s
Deploy / deploy-production (push) Has been skipped
deploy
2026-06-20 01:50:07 +02:00

314 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App;
final class ModuleTaskScheduler
{
public function __construct(
private ModuleManager $modules,
private ?\PDO $pdo
) {
}
public function definitions(string $moduleName): array
{
$module = $this->modules->get($moduleName);
$tasks = is_array($module['interval_tasks'] ?? null) ? $module['interval_tasks'] : [];
$definitions = [];
foreach ($tasks as $task) {
if (!is_array($task)) {
continue;
}
$name = trim((string) ($task['name'] ?? ''));
$callback = trim((string) ($task['callback'] ?? ''));
if ($name === '' || $callback === '') {
continue;
}
$definitions[$name] = [
'name' => $name,
'label' => trim((string) ($task['label'] ?? $name)),
'callback' => $callback,
'enabled_setting' => trim((string) ($task['enabled_setting'] ?? '')),
'interval_setting' => trim((string) ($task['interval_setting'] ?? '')),
'default_enabled' => array_key_exists('default_enabled', $task) ? (bool) $task['default_enabled'] : false,
'default_interval_hours' => max(1.0, (float) ($task['default_interval_hours'] ?? 6)),
'lock_minutes' => max(1, (int) ($task['lock_minutes'] ?? 10)),
];
}
return $definitions;
}
public function statuses(string $moduleName): array
{
$definitions = $this->definitions($moduleName);
if ($definitions === []) {
return [];
}
$settings = $this->modules->settings($moduleName);
$states = $this->fetchStates($moduleName);
$nowTs = time();
$statuses = [];
foreach ($definitions as $name => $definition) {
$state = $states[$name] ?? [];
$enabledSetting = $definition['enabled_setting'];
$intervalSetting = $definition['interval_setting'];
$enabled = $definition['default_enabled'];
if ($enabledSetting !== '') {
$enabled = $this->settingBool($settings[$enabledSetting] ?? null, $definition['default_enabled']);
}
$intervalHours = $definition['default_interval_hours'];
if ($intervalSetting !== '') {
$intervalHours = $this->settingFloat($settings[$intervalSetting] ?? null, $definition['default_interval_hours']);
}
$intervalHours = max(1.0, $intervalHours);
$referenceAt = trim((string) ($state['last_success_at'] ?? ''));
$referenceTs = $referenceAt !== '' ? strtotime($referenceAt) : false;
$nextDueTs = $referenceTs !== false ? ((int) $referenceTs + (int) round($intervalHours * 3600)) : null;
$isLocked = $this->isLockActive($state, $nowTs);
$isDue = $enabled && !$isLocked && ($nextDueTs === null || $nextDueTs <= $nowTs);
$statuses[] = $definition + [
'enabled' => $enabled,
'interval_hours' => $intervalHours,
'state' => $state,
'is_due' => $isDue,
'is_locked' => $isLocked,
'next_due_at' => $nextDueTs !== null ? gmdate('Y-m-d H:i:s', $nextDueTs) : null,
];
}
return $statuses;
}
public function runDue(string $moduleName): array
{
$results = [];
foreach ($this->statuses($moduleName) as $task) {
if (empty($task['enabled']) || empty($task['is_due'])) {
continue;
}
if (!$this->modules->hasFunction($moduleName, (string) $task['callback'])) {
$results[] = [
'task' => $task['name'],
'ok' => false,
'message' => 'Callback nicht registriert.',
];
continue;
}
if (!$this->acquireLock($moduleName, (string) $task['name'], (int) $task['lock_minutes'])) {
continue;
}
$startedAt = gmdate('Y-m-d H:i:s');
$this->persistState($moduleName, (string) $task['name'], [
'last_started_at' => $startedAt,
'last_status' => 'running',
'last_message' => 'Automatischer Lauf gestartet.',
]);
try {
$result = $this->modules->call($moduleName, (string) $task['callback'], [
'task' => $task,
'trigger' => 'interval_runner',
'started_at' => $startedAt,
]);
$ok = !is_array($result) || !array_key_exists('ok', $result) || !empty($result['ok']);
$skipped = is_array($result) && !empty($result['skipped']);
$message = is_array($result) ? trim((string) ($result['message'] ?? '')) : '';
$finishedAt = gmdate('Y-m-d H:i:s');
$payload = [
'last_finished_at' => $finishedAt,
'last_status' => $skipped ? 'skipped' : ($ok ? 'success' : 'error'),
'last_message' => $message,
'lock_until' => null,
];
if ($ok && !$skipped) {
$payload['last_success_at'] = $finishedAt;
}
$this->persistState($moduleName, (string) $task['name'], $payload);
$results[] = [
'task' => $task['name'],
'ok' => $ok,
'message' => $message,
];
} catch (\Throwable $e) {
$finishedAt = gmdate('Y-m-d H:i:s');
$this->persistState($moduleName, (string) $task['name'], [
'last_finished_at' => $finishedAt,
'last_status' => 'error',
'last_message' => $e->getMessage(),
'lock_until' => null,
]);
$results[] = [
'task' => $task['name'],
'ok' => false,
'message' => $e->getMessage(),
];
}
}
return $results;
}
private function fetchStates(string $moduleName): array
{
if (!$this->pdo) {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_module_task_runs
WHERE module_name = :module_name"
);
$stmt->execute(['module_name' => $moduleName]);
$states = [];
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
$name = trim((string) ($row['task_name'] ?? ''));
if ($name !== '') {
$states[$name] = $row;
}
}
return $states;
}
private function acquireLock(string $moduleName, string $taskName, int $lockMinutes): bool
{
if (!$this->pdo) {
return true;
}
$states = $this->fetchStates($moduleName);
$state = $states[$taskName] ?? [];
if ($this->isLockActive($state, time())) {
return false;
}
$lockUntil = gmdate('Y-m-d H:i:s', time() + ($lockMinutes * 60));
$this->persistState($moduleName, $taskName, [
'lock_until' => $lockUntil,
]);
return true;
}
private function persistState(string $moduleName, string $taskName, array $values): void
{
if (!$this->pdo) {
return;
}
$current = $this->fetchStates($moduleName)[$taskName] ?? [];
$payload = array_merge($current, $values, [
'module_name' => $moduleName,
'task_name' => $taskName,
'updated_at' => gmdate('Y-m-d H:i:s'),
]);
$params = [
'module_name' => $moduleName,
'task_name' => $taskName,
'last_started_at' => $this->nullOrString($payload['last_started_at'] ?? null),
'last_finished_at' => $this->nullOrString($payload['last_finished_at'] ?? null),
'last_success_at' => $this->nullOrString($payload['last_success_at'] ?? null),
'last_status' => $this->nullOrString($payload['last_status'] ?? null),
'last_message' => $this->nullOrString($payload['last_message'] ?? null),
'lock_until' => $this->nullOrString($payload['lock_until'] ?? null),
'updated_at' => $payload['updated_at'],
];
$updateStmt = $this->pdo->prepare(
"UPDATE nexus_module_task_runs
SET last_started_at = :last_started_at,
last_finished_at = :last_finished_at,
last_success_at = :last_success_at,
last_status = :last_status,
last_message = :last_message,
lock_until = :lock_until,
updated_at = :updated_at
WHERE module_name = :module_name
AND task_name = :task_name"
);
$updateStmt->execute($params);
if ($updateStmt->rowCount() > 0) {
return;
}
$insertStmt = $this->pdo->prepare(
"INSERT INTO nexus_module_task_runs (
module_name,
task_name,
last_started_at,
last_finished_at,
last_success_at,
last_status,
last_message,
lock_until,
updated_at
) VALUES (
:module_name,
:task_name,
:last_started_at,
:last_finished_at,
:last_success_at,
:last_status,
:last_message,
:lock_until,
:updated_at
)"
);
$insertStmt->execute($params);
}
private function nullOrString(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function settingBool(mixed $value, bool $default): bool
{
if ($value === null || $value === '') {
return $default;
}
return in_array((string) $value, ['1', 'true', 'yes', 'on'], true);
}
private function settingFloat(mixed $value, float $default): float
{
if (!is_numeric($value)) {
return $default;
}
$numeric = (float) $value;
return $numeric > 0 ? $numeric : $default;
}
private function isLockActive(array $state, int $nowTs): bool
{
$lockUntil = trim((string) ($state['lock_until'] ?? ''));
if ($lockUntil === '') {
return false;
}
$lockTs = strtotime($lockUntil);
return $lockTs !== false && $lockTs > $nowTs;
}
}