summaryrefslogtreecommitdiffstats
path: root/application/clicommands/ScheduleCommand.php
blob: f50d046b596b72b4ecb7ab1a60d52f32e01c5ed8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php

// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2

namespace Icinga\Module\Reporting\Clicommands;

use DateTime;
use Exception;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Data\ResourceFactory;
use Icinga\Module\Reporting\Cli\Command;
use Icinga\Module\Reporting\Database;
use Icinga\Module\Reporting\Model;
use Icinga\Module\Reporting\Report;
use Icinga\Module\Reporting\Schedule;
use ipl\Scheduler\Contract\Frequency;
use ipl\Scheduler\Contract\Task;
use ipl\Scheduler\Scheduler;
use React\EventLoop\Loop;
use React\Promise\ExtendedPromiseInterface;
use Throwable;

class ScheduleCommand extends Command
{
    /**
     * Run all configured reports based on their schedule
     *
     * USAGE:
     *
     *   icingacli reporting schedule run
     */
    public function runAction()
    {
        $scheduler = new Scheduler();
        $this->attachJobsLogging($scheduler);

        /** @var Schedule[] $runningSchedules */
        $runningSchedules = [];
        // Check for configuration changes every 5 minutes to make sure new jobs are scheduled, updated and deleted
        // jobs are cancelled.
        $watchdog = function () use (&$watchdog, $scheduler, &$runningSchedules) {
            $schedules = [];
            try {
                // Since this is a long-running daemon, the resources or module config may change meanwhile.
                // Therefore, reload the resources and module config from disk each time (at 5m intervals)
                // before reconnecting to the database.
                ResourceFactory::setConfig(Config::app('resources', true));
                Config::module('reporting', 'config', true);

                $schedules = $this->fetchSchedules();
            } catch (Throwable $err) {
                Logger::error('Failed to fetch report schedules from the database: %s', $err);
                Logger::debug($err->getTraceAsString());
            }

            $outdated = array_diff_key($runningSchedules, $schedules);
            foreach ($outdated as $schedule) {
                Logger::info(
                    'Removing %s as it either no longer exists in the database or its config has been changed',
                    $schedule->getName()
                );

                $scheduler->remove($schedule);

                unset($runningSchedules[$schedule->getUuid()->toString()]);
            }

            $newSchedules = array_diff_key($schedules, $runningSchedules);
            foreach ($newSchedules as $key => $schedule) {
                $config = $schedule->getConfig();
                $frequency = $config['frequency'];

                try {
                    /** @var Frequency $type */
                    $type = $config['frequencyType'];
                    $frequency = $type::fromJson($frequency);
                } catch (Exception $err) {
                    Logger::error(
                        '%s has invalid schedule expression %s: %s',
                        $schedule->getName(),
                        $frequency,
                        $err->getMessage()
                    );

                    continue;
                }

                $scheduler->schedule($schedule, $frequency);

                $runningSchedules[$key] = $schedule;
            }

            Loop::addTimer(5 * 60, $watchdog);
        };
        Loop::futureTick($watchdog);
    }

    /**
     * Fetch schedules from the database
     *
     * @return Schedule[]
     */
    protected function fetchSchedules(): array
    {
        $schedules = [];
        $query = Model\Schedule::on(Database::get())->with(['report.timeframe', 'report']);

        foreach ($query as $schedule) {
            $schedule = Schedule::fromModel($schedule, Report::fromModel($schedule->report));
            $schedules[$schedule->getUuid()->toString()] = $schedule;
        }

        return $schedules;
    }

    protected function attachJobsLogging(Scheduler $scheduler)
    {
        $scheduler->on(Scheduler::ON_TASK_FAILED, function (Task $job, Throwable $e) {
            Logger::error('Failed to run job %s: %s', $job->getName(), $e->getMessage());
            Logger::debug($e->getTraceAsString());
        });

        $scheduler->on(Scheduler::ON_TASK_RUN, function (Task $job, ExtendedPromiseInterface $_) {
            Logger::info('Running job %s', $job->getName());
        });

        $scheduler->on(Scheduler::ON_TASK_SCHEDULED, function (Task $job, DateTime $dateTime) {
            Logger::info('Scheduling job %s to run at %s', $job->getName(), $dateTime->format('Y-m-d H:i:s'));
        });

        $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Task $task, DateTime $dateTime) {
            Logger::info(
                sprintf('Detaching expired job %s at %s', $task->getName(), $dateTime->format('Y-m-d H:i:s'))
            );
        });
    }
}