Passed
Branch feature/2.0 (ef99fd)
by Jonathan
11:25
created

TaskScheduleService::run()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 24
c 2
b 0
f 0
nc 5
nop 2
dl 0
loc 33
rs 8.4444
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, 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\CarbonInterval;
18
use Fisharebest\Webtrees\Carbon;
19
use Fisharebest\Webtrees\Services\ModuleService;
20
use Illuminate\Database\Capsule\Manager as DB;
21
use Illuminate\Database\Query\Builder;
22
use Illuminate\Support\Collection;
23
use MyArtJaub\Webtrees\Module\AdminTasks\Model\ModuleTasksProviderInterface;
24
use MyArtJaub\Webtrees\Module\AdminTasks\Model\TaskInterface;
25
use MyArtJaub\Webtrees\Module\AdminTasks\Model\TaskSchedule;
26
use Closure;
27
use Exception;
28
use stdClass;
29
30
/**
31
 * Service for Task Schedules CRUD, and tasks execution
32
 *
33
 */
34
class TaskScheduleService
35
{
36
    /**
37
     * Time-out after which the task will be considered not running any more.
38
     * In seconds, default 5 mins.
39
     * @var integer
40
     */
41
    public const TASK_TIME_OUT = 600;
42
    
43
    /**
44
     * @var Collection $available_tasks
45
     */
46
    private $available_tasks;
47
    
48
    /**
49
     * Returns all Tasks schedules in database.
50
     * Stored records can be synchronised with the tasks actually available to the system.
51
     *
52
     * @param bool $sync_available Should tasks synchronised with available ones
53
     * @param bool $include_disabled Should disabled tasks be returned
54
     * @return Collection Collection of TaskSchedule
55
     */
56
    public function all(bool $sync_available = false, bool $include_disabled = true): Collection
57
    {
58
        $tasks_schedules = DB::table('maj_admintasks')
59
        ->select()
60
        ->get()
61
        ->map(self::rowMapper());
62
        
63
        if ($sync_available) {
64
            $available_tasks = clone $this->available();
65
            foreach ($tasks_schedules as $task_schedule) {
66
                /** @var TaskSchedule $task_schedule */
67
                if ($available_tasks->has($task_schedule->taskId())) {
68
                    $available_tasks->forget($task_schedule->taskId());
69
                } else {
70
                    $this->delete($task_schedule);
71
                }
72
            }
73
            
74
            foreach ($available_tasks as $task_name => $task) {
75
                /** @var TaskInterface $task */
76
                $this->insertTask($task_name, $task->defaultFrequency());
77
            }
78
            
79
            return $this->all(false, $include_disabled);
80
        }
81
        
82
        return $tasks_schedules;
83
    }
84
    
85
    /**
86
     * Returns tasks exposed through modules implementing ModuleTasksProviderInterface.
87
     *
88
     * @return Collection
89
     */
90
    public function available(): Collection
91
    {
92
        if ($this->available_tasks === null) {
93
            $tasks_providers = app(ModuleService::class)->findByInterface(ModuleTasksProviderInterface::class);
94
            
95
            $this->available_tasks = new Collection();
96
            foreach ($tasks_providers as $task_provider) {
97
                $this->available_tasks = $this->available_tasks->merge($task_provider->listTasks());
98
            }
99
        }
100
        return $this->available_tasks;
101
    }
102
    
103
    /**
104
     * Find a task schedule by its ID.
105
     *
106
     * @param int $task_schedule_id
107
     * @return TaskSchedule|NULL
108
     */
109
    public function find(int $task_schedule_id): ?TaskSchedule
110
    {
111
        return DB::table('maj_admintasks')
112
            ->select()
113
            ->where('majat_id', '=', $task_schedule_id)
114
            ->get()
115
            ->map(self::rowMapper())
116
            ->first();
117
    }
118
    
119
    /**
120
     * Add a new task schedule with the specified task ID, and frequency if defined.
121
     * Uses default for other settings.
122
     *
123
     * @param string $task_id
124
     * @param int $frequency
125
     * @return bool
126
     */
127
    public function insertTask(string $task_id, int $frequency = 0): bool
128
    {
129
        $values = ['majat_task_id' => $task_id];
130
        if ($frequency > 0) {
131
            $values['majat_frequency'] = $frequency;
132
        }
133
        
134
        return DB::table('maj_admintasks')
135
            ->insert($values);
136
    }
137
    
138
    /**
139
     * Update a task schedule.
140
     * Returns the number of tasks schedules updated.
141
     *
142
     * @param TaskSchedule $task_schedule
143
     * @return int
144
     */
145
    public function update(TaskSchedule $task_schedule): int
146
    {
147
        return DB::table('maj_admintasks')
148
            ->where('majat_id', '=', $task_schedule->id())
149
            ->update([
150
                'majat_status'      =>  $task_schedule->isEnabled() ? 'enabled' : 'disabled',
151
                'majat_last_run'    =>  $task_schedule->lastRunTime(),
152
                'majat_last_result' =>  $task_schedule->wasLastRunSuccess(),
153
                'majat_frequency'   =>  $task_schedule->frequency()->totalMinutes,
154
                'majat_nb_occur'    =>  $task_schedule->remainingOccurences(),
155
                'majat_running'     =>  $task_schedule->isRunning()
156
            ]);
157
    }
158
    
159
    /**
160
     * Delete a task schedule.
161
     *
162
     * @param TaskSchedule $task_schedule
163
     * @return int
164
     */
165
    public function delete(TaskSchedule $task_schedule): int
166
    {
167
        return DB::table('maj_admintasks')
168
            ->where('majat_id', '=', $task_schedule->id())
169
            ->delete();
170
    }
171
    
172
    /**
173
     * Find a task by its name
174
     *
175
     * @param string $task_id
176
     * @return TaskInterface|NULL
177
     */
178
    public function findTask(string $task_id): ?TaskInterface
179
    {
180
        if ($this->available()->has($task_id)) {
181
            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 Fisharebest\Webtrees\Application which is incompatible with the type-hinted return MyArtJaub\Webtrees\Modul...odel\TaskInterface|null. Consider adding an additional type-check to rule them out.
Loading history...
182
        }
183
        return null;
184
    }
185
    
186
    /**
187
     * Retrieve all tasks that are candidates to be run.
188
     *
189
     * @param bool $force Should the run be forced
190
     * @param string $task_id Specific task ID to be run
191
     * @return Collection
192
     */
193
    public function findTasksToRun(bool $force, string $task_id = null): Collection
194
    {
195
        $query = DB::table('maj_admintasks')
196
            ->select()
197
            ->where('majat_status', '=', 'enabled')
198
            ->where(function (Builder $query) {
199
200
                $query->where('majat_running', '=', 0)
201
                ->orWhere('majat_last_run', '<=', Carbon::now()->subSeconds(self::TASK_TIME_OUT));
202
            });
203
            
204
        if (!$force) {
205
            $query->where(function (Builder $query) {
206
207
                $query->where('majat_running', '=', 0)
208
                    ->orWhereRaw('DATE_ADD(majat_last_run, INTERVAL majat_frequency MINUTE) <= NOW()');
209
            });
210
        }
211
        
212
        if ($task_id !== null) {
213
            $query->where('majat_task_id', '=', $task_id);
214
        }
215
        
216
        return $query->get()->map(self::rowMapper());
217
    }
218
    
219
    /**
220
     * Run the task associated with the schedule.
221
     * The task will run if either forced to, or its next scheduled run time has been exceeded.
222
     * The last run time is recorded only if the task is successful.
223
     *
224
     * @param TaskSchedule $task_schedule
225
     * @param boolean $force
226
     */
227
    public function run(TaskSchedule $task_schedule, $force = false): void
228
    {
229
        /** @var TaskSchedule $task_schedule */
230
        $task_schedule = DB::table('maj_admintasks')
231
            ->select()
232
            ->where('majat_id', '=', $task_schedule->id())
233
            ->lockForUpdate()
234
            ->get()
235
            ->map(self::rowMapper())
236
            ->first();
237
        
238
        if (
239
            !$task_schedule->isRunning() &&
240
            ($force || $task_schedule->lastRunTime()->add($task_schedule->frequency())->lessThan(Carbon::now())) &&
241
            $task_schedule->setLastResult(false) &&  // @phpstan-ignore-line  Used as setter, not as a condition
242
            $task = $this->findTask($task_schedule->taskId())
243
        ) {
244
            $task_schedule->startRunning();
245
            $this->update($task_schedule);
246
            
247
            try {
248
                $task_schedule->setLastResult($task->run($task_schedule));
249
            } catch (Exception $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
250
            }
251
            
252
            if ($task_schedule->wasLastRunSuccess()) {
253
                $task_schedule->setLastRunTime(Carbon::now());
254
                $task_schedule->decrementRemainingOccurences();
255
            }
256
            $task_schedule->stopRunning();
257
            $this->update($task_schedule);
258
        } else {
259
            $this->update($task_schedule);
260
        }
261
    }
262
263
    /**
264
     * Mapper to return a TaskSchedule object from an object.
265
     *
266
     * @return Closure
267
     */
268
    public static function rowMapper(): Closure
269
    {
270
        return static function (stdClass $row): TaskSchedule {
271
272
            return new TaskSchedule(
273
                (int) $row->majat_id,
274
                $row->majat_task_id,
275
                $row->majat_status === 'enabled',
276
                Carbon::parse($row->majat_last_run),
277
                (bool) $row->majat_last_result,
278
                CarbonInterval::minutes($row->majat_frequency),
279
                (int) $row->majat_nb_occur,
280
                (bool) $row->majat_running
281
            );
282
        };
283
    }
284
}
285