TaskScheduleService::find()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees-lib: MyArtJaub library for webtrees
5
 *
6
 * @package MyArtJaub\Webtrees
7
 * @subpackage AdminTasks
8
 * @author Jonathan Jaubart <[email protected]>
9
 * @copyright Copyright (c) 2020-2022, Jonathan Jaubart
10
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3
11
 */
12
13
declare(strict_types=1);
14
15
namespace MyArtJaub\Webtrees\Module\AdminTasks\Services;
16
17
use Carbon\CarbonImmutable;
18
use Fisharebest\Webtrees\I18N;
19
use Fisharebest\Webtrees\Log;
20
use Fisharebest\Webtrees\Registry;
21
use Fisharebest\Webtrees\Services\ModuleService;
22
use Illuminate\Database\Capsule\Manager as DB;
23
use Illuminate\Database\Query\Builder;
24
use Illuminate\Support\Collection;
25
use MyArtJaub\Webtrees\Common\Tasks\TaskSchedule;
26
use MyArtJaub\Webtrees\Contracts\Tasks\ModuleTasksProviderInterface;
27
use MyArtJaub\Webtrees\Contracts\Tasks\TaskInterface;
28
use Closure;
29
use Throwable;
30
use stdClass;
31
32
/**
33
 * Service for Task Schedules CRUD, and tasks execution
34
 *
35
 */
36
class TaskScheduleService
37
{
38
    /**
39
     * Time-out after which the task will be considered not running any more.
40
     * In seconds, default 5 mins.
41
     * @var integer
42
     */
43
    public const TASK_TIME_OUT = 600;
44
45
    private ModuleService $module_service;
46
47
    /**
48
     * Constructor for TaskScheduleService
49
     *
50
     * @param ModuleService $module_service
51
     */
52
    public function __construct(ModuleService $module_service)
53
    {
54
        $this->module_service = $module_service;
55
    }
56
57
    /**
58
     * Returns all Tasks schedules in database.
59
     * Stored records can be synchronised with the tasks actually available to the system.
60
     *
61
     * @param bool $sync_available Should tasks synchronised with available ones
62
     * @param bool $include_disabled Should disabled tasks be returned
63
     * @return Collection<TaskSchedule> Collection of TaskSchedule
64
     */
65
    public function all(bool $sync_available = false, bool $include_disabled = true): Collection
66
    {
67
        $tasks_schedules = DB::table('maj_admintasks')
68
            ->select()
69
            ->get()
70
            ->map(self::rowMapper());
71
72
        if ($sync_available) {
73
            $available_tasks = clone $this->available();
74
            foreach ($tasks_schedules as $task_schedule) {
75
                /** @var TaskSchedule $task_schedule */
76
                if ($available_tasks->has($task_schedule->taskId())) {
77
                    $available_tasks->forget($task_schedule->taskId());
78
                } else {
79
                    $this->delete($task_schedule);
80
                }
81
            }
82
83
            foreach ($available_tasks as $task_name => $task_class) {
84
                if (null !== $task = app($task_class)) {
85
                    $this->insertTask($task_name, $task->defaultFrequency());
0 ignored issues
show
Bug introduced by
The method defaultFrequency() does not exist on Illuminate\Container\Container. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

85
                    $this->insertTask($task_name, $task->/** @scrutinizer ignore-call */ defaultFrequency());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
86
                }
87
            }
88
89
            return $this->all(false, $include_disabled);
90
        }
91
92
        return $tasks_schedules;
93
    }
94
95
    /**
96
     * Returns tasks exposed through modules implementing ModuleTasksProviderInterface.
97
     *
98
     * @return Collection<string, string>
99
     */
100
    public function available(): Collection
101
    {
102
        return Registry::cache()->array()->remember(
103
            'maj-available-admintasks',
104
            function (): Collection {
105
                /** @var Collection<string, string> $tasks */
106
                $tasks = $this->module_service
107
                    ->findByInterface(ModuleTasksProviderInterface::class)
108
                    ->flatMap(fn(ModuleTasksProviderInterface $module) => $module->listTasks());
109
                return $tasks;
110
            }
111
        );
112
    }
113
114
    /**
115
     * Find a task schedule by its ID.
116
     *
117
     * @param int $task_schedule_id
118
     * @return TaskSchedule|NULL
119
     */
120
    public function find(int $task_schedule_id): ?TaskSchedule
121
    {
122
        return DB::table('maj_admintasks')
123
            ->select()
124
            ->where('majat_id', '=', $task_schedule_id)
125
            ->get()
126
            ->map(self::rowMapper())
127
            ->first();
128
    }
129
130
    /**
131
     * Add a new task schedule with the specified task ID, and frequency if defined.
132
     * Uses default for other settings.
133
     *
134
     * @param string $task_id
135
     * @param int $frequency
136
     * @return bool
137
     */
138
    public function insertTask(string $task_id, int $frequency = 0): bool
139
    {
140
        $values = ['majat_task_id' => $task_id];
141
        if ($frequency > 0) {
142
            $values['majat_frequency'] = $frequency;
143
        }
144
145
        return DB::table('maj_admintasks')
146
            ->insert($values);
147
    }
148
149
    /**
150
     * Update a task schedule.
151
     * Returns the number of tasks schedules updated.
152
     *
153
     * @param TaskSchedule $task_schedule
154
     * @return int
155
     */
156
    public function update(TaskSchedule $task_schedule): int
157
    {
158
        return DB::table('maj_admintasks')
159
            ->where('majat_id', '=', $task_schedule->id())
160
            ->update([
161
                'majat_status'      =>  $task_schedule->isEnabled() ? 'enabled' : 'disabled',
162
                'majat_last_run'    =>  $task_schedule->lastRunTime()->toDateTimeString(),
163
                'majat_last_result' =>  $task_schedule->wasLastRunSuccess(),
164
                'majat_frequency'   =>  $task_schedule->frequency(),
165
                'majat_nb_occur'    =>  $task_schedule->remainingOccurrences(),
166
                'majat_running'     =>  $task_schedule->isRunning()
167
            ]);
168
    }
169
170
    /**
171
     * Delete a task schedule.
172
     *
173
     * @param TaskSchedule $task_schedule
174
     * @return int
175
     */
176
    public function delete(TaskSchedule $task_schedule): int
177
    {
178
        return DB::table('maj_admintasks')
179
            ->where('majat_id', '=', $task_schedule->id())
180
            ->delete();
181
    }
182
183
    /**
184
     * Find a task by its name
185
     *
186
     * @param string $task_id
187
     * @return TaskInterface|NULL
188
     */
189
    public function findTask(string $task_id): ?TaskInterface
190
    {
191
        if ($this->available()->has($task_id)) {
192
            return app($this->available()->get($task_id));
0 ignored issues
show
Bug Best Practice introduced by
The expression return app($this->available()->get($task_id)) could return the type Illuminate\Container\Container which is incompatible with the type-hinted return MyArtJaub\Webtrees\Contr...asks\TaskInterface|null. Consider adding an additional type-check to rule them out.
Loading history...
193
        }
194
        return null;
195
    }
196
197
    /**
198
     * Retrieve all tasks that are candidates to be run.
199
     *
200
     * @param bool $force Should the run be forced
201
     * @param string $task_id Specific task ID to be run
202
     * @return Collection<TaskSchedule>
203
     */
204
    public function findTasksToRun(bool $force, string $task_id = ''): Collection
205
    {
206
        $query = DB::table('maj_admintasks')
207
            ->select()
208
            ->where('majat_status', '=', 'enabled')
209
            ->where(function (Builder $query): void {
210
                $query->where('majat_running', '=', 0)
211
                    ->orWhere('majat_last_run', '<=', CarbonImmutable::now('UTC')->subSeconds(self::TASK_TIME_OUT));
212
            });
213
214
        if (!$force) {
215
            $query->where(function (Builder $query): void {
216
217
                $query->where('majat_last_result', '=', 0)
218
                    ->orWhereRaw('DATE_ADD(majat_last_run, INTERVAL majat_frequency MINUTE) <= NOW()');
219
            });
220
        }
221
222
        if ($task_id !== '') {
223
            $query->where('majat_task_id', '=', $task_id);
224
        }
225
226
        return $query->get()->map(self::rowMapper());
227
    }
228
229
    /**
230
     * Run the task associated with the schedule.
231
     * The task will run if either forced to, or its next scheduled run time has been exceeded.
232
     * The last run time is recorded only if the task is successful.
233
     *
234
     * @param TaskSchedule $task_schedule
235
     * @param boolean $force
236
     */
237
    public function run(TaskSchedule $task_schedule, $force = false): void
238
    {
239
        /** @var TaskSchedule $task_schedule */
240
        $task_schedule = DB::table('maj_admintasks')
241
            ->select()
242
            ->where('majat_id', '=', $task_schedule->id())
243
            ->lockForUpdate()
244
            ->get()
245
            ->map(self::rowMapper())
246
            ->first();
247
248
        if (
249
            !$task_schedule->isRunning() &&
250
            ($force ||
251
                $task_schedule->lastRunTime()->addMinutes($task_schedule->frequency())
252
                    ->lessThan(CarbonImmutable::now('UTC'))
253
            )
254
        ) {
255
            $task_schedule->setLastResult(false);
256
257
            $task = $this->findTask($task_schedule->taskId());
258
            if ($task !== null) {
259
                $task_schedule->startRunning();
260
                $this->update($task_schedule);
261
262
                $first_error = $task_schedule->wasLastRunSuccess();
263
                try {
264
                    $task_schedule->setLastResult($task->run($task_schedule));
265
                } catch (Throwable $ex) {
266
                    if ($first_error) { // Only record the first error, as this could fill the log.
0 ignored issues
show
introduced by
The condition $first_error is always false.
Loading history...
267
                        Log::addErrorLog(I18N::translate('Error while running task %s:', $task->name()) . ' ' .
268
                            '[' . get_class($ex) . '] ' . $ex->getMessage() . ' ' . $ex->getFile() . ':'
269
                            . $ex->getLine() . PHP_EOL . $ex->getTraceAsString());
270
                    }
271
                }
272
273
                if ($task_schedule->wasLastRunSuccess()) {
274
                    $task_schedule->setLastRunTime(CarbonImmutable::now('UTC'));
275
                    $task_schedule->decrementRemainingOccurrences();
276
                }
277
                $task_schedule->stopRunning();
278
            }
279
            $this->update($task_schedule);
280
        }
281
    }
282
283
    /**
284
     * Mapper to return a TaskSchedule object from an object.
285
     *
286
     * @return Closure(stdClass $row): TaskSchedule
287
     */
288
    public static function rowMapper(): Closure
289
    {
290
        return static function (stdClass $row): TaskSchedule {
291
            return new TaskSchedule(
292
                (int) $row->majat_id,
293
                $row->majat_task_id,
294
                $row->majat_status === 'enabled',
295
                CarbonImmutable::parse($row->majat_last_run, 'UTC'),
296
                (bool) $row->majat_last_result,
297
                (int) $row->majat_frequency,
298
                (int) $row->majat_nb_occur,
299
                (bool) $row->majat_running
300
            );
301
        };
302
    }
303
}
304