PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Scheduler / Scheduler.php
matomo / app / core / Scheduler Last commit date
Schedule 1 month ago RetryableException.php 2 years ago ScheduledTaskLock.php 2 years ago Scheduler.php 1 year ago Task.php 1 month ago TaskLoader.php 6 months ago Timetable.php 1 month ago
Scheduler.php
325 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Scheduler;
10
11 use Piwik\Concurrency\Lock;
12 use Piwik\Piwik;
13 use Piwik\Timer;
14 use Piwik\Log\LoggerInterface;
15 /**
16 * Schedules task execution.
17 *
18 * A scheduled task is a callback that should be executed every so often (such as daily,
19 * weekly, monthly, etc.). They are registered by extending {@link \Piwik\Plugin\Tasks}.
20 *
21 * Tasks are executed when the `core:archive` command is executed.
22 *
23 * ### Examples
24 *
25 * **Scheduling a task**
26 *
27 * class Tasks extends \Piwik\Plugin\Tasks
28 * {
29 * public function schedule()
30 * {
31 * $this->hourly('myTask'); // myTask() will be executed once every hour
32 * }
33 * public function myTask()
34 * {
35 * // do something
36 * }
37 * }
38 *
39 * **Executing all pending tasks**
40 *
41 * $results = $scheduler->run();
42 * $task1Result = $results[0];
43 * $task1Name = $task1Result['task'];
44 * $task1Output = $task1Result['output'];
45 *
46 * echo "Executed task '$task1Name'. Task output:\n$task1Output";
47 */
48 class Scheduler
49 {
50 /**
51 * Is the scheduler running any task.
52 * @var bool
53 */
54 private $isRunningTask = \false;
55 /**
56 * Should the last run task be scheduled for a retry
57 * @var bool
58 */
59 private $scheduleRetry = \false;
60 /**
61 * @var Timetable
62 */
63 private $timetable;
64 /**
65 * @var TaskLoader
66 */
67 private $loader;
68 /**
69 * @var LoggerInterface
70 */
71 private $logger;
72 /**
73 * @var Lock
74 */
75 private $lock;
76 /**
77 * @var bool
78 */
79 private $hasReceivedAbortSignal = \false;
80 public function __construct(\Piwik\Scheduler\TaskLoader $loader, LoggerInterface $logger, \Piwik\Scheduler\ScheduledTaskLock $lock)
81 {
82 $this->timetable = new \Piwik\Scheduler\Timetable();
83 $this->loader = $loader;
84 $this->logger = $logger;
85 $this->lock = $lock;
86 }
87 public function handleSignal(int $signal) : void
88 {
89 $this->hasReceivedAbortSignal = in_array($signal, [\SIGINT, \SIGTERM], \true);
90 }
91 /**
92 * Executes tasks that are scheduled to run, then reschedules them.
93 *
94 * @return array An array describing the results of scheduled task execution. Each element
95 * in the array will have the following format:
96 *
97 * ```
98 * array(
99 * 'task' => 'task name',
100 * 'output' => '... task output ...'
101 * )
102 * ```
103 */
104 public function run()
105 {
106 $tasks = $this->loader->loadTasks();
107 $this->logger->debug('{count} scheduled tasks loaded', array('count' => count($tasks)));
108 // remove from timetable tasks that are not active anymore
109 $this->timetable->removeInactiveTasks($tasks);
110 $this->logger->info("Starting Scheduled tasks... ");
111 // for every priority level, starting with the highest and concluding with the lowest
112 $executionResults = array();
113 $readFromOption = \true;
114 for ($priority = \Piwik\Scheduler\Task::HIGHEST_PRIORITY; $priority <= \Piwik\Scheduler\Task::LOWEST_PRIORITY; ++$priority) {
115 $this->logger->debug("Executing tasks with priority {priority}:", array('priority' => $priority));
116 // loop through each task
117 foreach ($tasks as $task) {
118 if ($this->hasReceivedAbortSignal) {
119 $this->logger->info("Scheduler: Aborting due to received signal");
120 return $executionResults;
121 }
122 // if the task does not have the current priority level, don't execute it yet
123 if ($task->getPriority() != $priority) {
124 continue;
125 }
126 $taskName = $task->getName();
127 if (!$this->acquireLockForTask($taskName, $task->getTTL())) {
128 $this->logger->debug("Scheduler: '{task}' is currently executed by another process", ['task' => $task->getName()]);
129 continue;
130 }
131 if ($readFromOption) {
132 // because other jobs might execute the scheduled tasks as well we have to read the up to date time table to not handle the same task twice
133 // ideally we would read from option every time but using $readFromOption as a minor performance tweak. There can be easily 100 tasks
134 // of which we only execute very few and it's unlikely that the timetable changes too much in between while iterating over the loop and triggering the event.
135 // this way we only read from option when we actually execute or reschedule a task as this can take a few seconds.
136 $this->timetable->readFromOption();
137 $readFromOption = \false;
138 }
139 $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName);
140 if ($this->timetable->taskShouldBeRescheduled($taskName)) {
141 $readFromOption = \true;
142 $rescheduledDate = $this->timetable->rescheduleTask($task);
143 $this->logger->debug("Task {task} is scheduled to run again for {date}.", array('task' => $taskName, 'date' => $rescheduledDate));
144 }
145 /**
146 * Triggered before a task is executed.
147 *
148 * A plugin can listen to it and modify whether a specific task should be executed or not. This way
149 * you can force certain tasks to be executed more often or for example to be never executed.
150 *
151 * @param bool &$shouldExecuteTask Decides whether the task will be executed.
152 * @param Task $task The task that is about to be executed.
153 */
154 Piwik::postEvent('ScheduledTasks.shouldExecuteTask', array(&$shouldExecuteTask, $task));
155 if ($shouldExecuteTask) {
156 $readFromOption = \true;
157 $this->scheduleRetry = \false;
158 $message = $this->executeTask($task);
159 // Task has thrown an exception and should be scheduled for a retry
160 if ($this->scheduleRetry) {
161 if ($this->timetable->getRetryCount($taskName) == 3) {
162 // Task has already been retried three times, give up
163 $this->timetable->clearRetryCount($taskName);
164 $this->logger->warning("Scheduler: '{task}' has already been retried three times, giving up", ['task' => $taskName]);
165 } else {
166 $readFromOption = \true;
167 $rescheduledDate = $this->timetable->rescheduleTaskAndRunInOneHour($task);
168 $this->timetable->incrementRetryCount($taskName);
169 $this->logger->info("Scheduler: '{task}' retry scheduled for {date}", ['task' => $taskName, 'date' => $rescheduledDate]);
170 }
171 $this->scheduleRetry = \false;
172 } else {
173 if ($this->timetable->getRetryCount($taskName) > 0) {
174 $this->timetable->clearRetryCount($taskName);
175 }
176 }
177 $executionResults[] = array('task' => $taskName, 'output' => $message);
178 }
179 $this->releaseLock();
180 }
181 }
182 $this->logger->info("done");
183 return $executionResults;
184 }
185 /**
186 * Run a specific task now. Will ignore the schedule completely.
187 *
188 * @param string $taskName
189 * @return string Task output.
190 */
191 public function runTaskNow($taskName)
192 {
193 $tasks = $this->loader->loadTasks();
194 foreach ($tasks as $task) {
195 if ($task->getName() === $taskName) {
196 if (!$this->acquireLockForTask($taskName, $task->getTTL())) {
197 return 'Execution skipped. Another process is currently executing this task.';
198 }
199 $result = $this->executeTask($task);
200 $this->releaseLock();
201 return $result;
202 }
203 }
204 throw new \InvalidArgumentException('Task ' . $taskName . ' not found');
205 }
206 /**
207 * Determines a task's scheduled time and persists it, overwriting the previous scheduled time.
208 *
209 * Call this method if your task's scheduled time has changed due to, for example, an option that
210 * was changed.
211 *
212 * @param Task $task Describes the scheduled task being rescheduled.
213 * @api
214 */
215 public function rescheduleTask(\Piwik\Scheduler\Task $task)
216 {
217 $this->logger->debug('Rescheduling task {task}', array('task' => $task->getName()));
218 $this->timetable->rescheduleTask($task);
219 }
220 /**
221 * Determines a task's scheduled time and persists it, overwriting the previous scheduled time.
222 *
223 * Call this method if your task's scheduled time has changed due to, for example, an option that
224 * was changed.
225 *
226 * The task will be run the first time tomorrow.
227 *
228 * @param Task $task Describes the scheduled task being rescheduled.
229 * @api
230 */
231 public function rescheduleTaskAndRunTomorrow(\Piwik\Scheduler\Task $task)
232 {
233 $this->logger->debug('Rescheduling task and setting first run for tomorrow {task}', array('task' => $task->getName()));
234 $this->timetable->rescheduleTaskAndRunTomorrow($task);
235 }
236 /**
237 * Returns true if the scheduler is currently running a task.
238 *
239 * @return bool
240 */
241 public function isRunningTask()
242 {
243 return $this->isRunningTask;
244 }
245 /**
246 * Return the next scheduled time given the class and method names of a scheduled task.
247 *
248 * @param string $className The name of the class that contains the scheduled task method.
249 * @param string $methodName The name of the scheduled task method.
250 * @param string|null $methodParameter Optional method parameter.
251 * @return mixed int|bool The time in milliseconds when the scheduled task will be executed
252 * next or false if it is not scheduled to run.
253 */
254 public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null)
255 {
256 return $this->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter);
257 }
258 /**
259 * Returns the list of the task names.
260 *
261 * @return string[]
262 */
263 public function getTaskList()
264 {
265 $tasks = $this->loader->loadTasks();
266 return array_map(function (\Piwik\Scheduler\Task $task) {
267 return $task->getName();
268 }, $tasks);
269 }
270 private function acquireLockForTask(string $taskName, int $ttlInSeconds) : bool
271 {
272 if (-1 === $ttlInSeconds) {
273 // lock disabled, so don't try to acquire one
274 return \true;
275 }
276 return $this->lock->acquireLock($taskName, $ttlInSeconds);
277 }
278 private function releaseLock()
279 {
280 $this->lock->unlock();
281 }
282 /**
283 * Executes the given task
284 *
285 * @param Task $task
286 * @return string
287 */
288 private function executeTask($task)
289 {
290 $this->logger->info("Scheduler: executing task {taskName}...", array('taskName' => $task->getName()));
291 $this->isRunningTask = \true;
292 $timer = new Timer();
293 /**
294 * Triggered directly before a scheduled task is executed
295 *
296 * @param Task $task The task that is about to be executed
297 */
298 Piwik::postEvent('ScheduledTasks.execute', array(&$task));
299 try {
300 $callable = array($task->getObjectInstance(), $task->getMethodName());
301 call_user_func($callable, $task->getMethodParameter());
302 $message = $timer->__toString();
303 } catch (\Exception $e) {
304 $this->logger->error("Scheduler: Error {errorMessage} for task '{task}'", ['errorMessage' => $e->getMessage(), 'task' => $task->getName()]);
305 $message = 'ERROR: ' . $e->getMessage();
306 // If the task has indicated that retrying on exception is safe then flag for rescheduling
307 if ($e instanceof \Piwik\Scheduler\RetryableException) {
308 $this->scheduleRetry = \true;
309 }
310 }
311 $this->isRunningTask = \false;
312 /**
313 * Triggered after a scheduled task is successfully executed.
314 *
315 * You can use the event to execute for example another task whenever a specific task is executed or to clean up
316 * certain resources.
317 *
318 * @param Task $task The task that was just executed
319 */
320 Piwik::postEvent('ScheduledTasks.execute.end', array(&$task));
321 $this->logger->info("Scheduler: finished. {timeElapsed}", array('timeElapsed' => $timer));
322 return $message;
323 }
324 }
325