PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
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 year ago RetryableException.php 2 years ago ScheduledTaskLock.php 2 years ago Scheduler.php 1 year ago Task.php 1 year ago TaskLoader.php 2 years ago Timetable.php 1 year ago
Scheduler.php
313 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 public function __construct(\Piwik\Scheduler\TaskLoader $loader, LoggerInterface $logger, \Piwik\Scheduler\ScheduledTaskLock $lock)
77 {
78 $this->timetable = new \Piwik\Scheduler\Timetable();
79 $this->loader = $loader;
80 $this->logger = $logger;
81 $this->lock = $lock;
82 }
83 /**
84 * Executes tasks that are scheduled to run, then reschedules them.
85 *
86 * @return array An array describing the results of scheduled task execution. Each element
87 * in the array will have the following format:
88 *
89 * ```
90 * array(
91 * 'task' => 'task name',
92 * 'output' => '... task output ...'
93 * )
94 * ```
95 */
96 public function run()
97 {
98 $tasks = $this->loader->loadTasks();
99 $this->logger->debug('{count} scheduled tasks loaded', array('count' => count($tasks)));
100 // remove from timetable tasks that are not active anymore
101 $this->timetable->removeInactiveTasks($tasks);
102 $this->logger->info("Starting Scheduled tasks... ");
103 // for every priority level, starting with the highest and concluding with the lowest
104 $executionResults = array();
105 $readFromOption = \true;
106 for ($priority = \Piwik\Scheduler\Task::HIGHEST_PRIORITY; $priority <= \Piwik\Scheduler\Task::LOWEST_PRIORITY; ++$priority) {
107 $this->logger->debug("Executing tasks with priority {priority}:", array('priority' => $priority));
108 // loop through each task
109 foreach ($tasks as $task) {
110 // if the task does not have the current priority level, don't execute it yet
111 if ($task->getPriority() != $priority) {
112 continue;
113 }
114 $taskName = $task->getName();
115 if (!$this->acquireLockForTask($taskName, $task->getTTL())) {
116 $this->logger->debug("Scheduler: '{task}' is currently executed by another process", ['task' => $task->getName()]);
117 continue;
118 }
119 if ($readFromOption) {
120 // 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
121 // ideally we would read from option every time but using $readFromOption as a minor performance tweak. There can be easily 100 tasks
122 // 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.
123 // this way we only read from option when we actually execute or reschedule a task as this can take a few seconds.
124 $this->timetable->readFromOption();
125 $readFromOption = \false;
126 }
127 $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName);
128 if ($this->timetable->taskShouldBeRescheduled($taskName)) {
129 $readFromOption = \true;
130 $rescheduledDate = $this->timetable->rescheduleTask($task);
131 $this->logger->debug("Task {task} is scheduled to run again for {date}.", array('task' => $taskName, 'date' => $rescheduledDate));
132 }
133 /**
134 * Triggered before a task is executed.
135 *
136 * A plugin can listen to it and modify whether a specific task should be executed or not. This way
137 * you can force certain tasks to be executed more often or for example to be never executed.
138 *
139 * @param bool &$shouldExecuteTask Decides whether the task will be executed.
140 * @param Task $task The task that is about to be executed.
141 */
142 Piwik::postEvent('ScheduledTasks.shouldExecuteTask', array(&$shouldExecuteTask, $task));
143 if ($shouldExecuteTask) {
144 $readFromOption = \true;
145 $this->scheduleRetry = \false;
146 $message = $this->executeTask($task);
147 // Task has thrown an exception and should be scheduled for a retry
148 if ($this->scheduleRetry) {
149 if ($this->timetable->getRetryCount($taskName) == 3) {
150 // Task has already been retried three times, give up
151 $this->timetable->clearRetryCount($taskName);
152 $this->logger->warning("Scheduler: '{task}' has already been retried three times, giving up", ['task' => $taskName]);
153 } else {
154 $readFromOption = \true;
155 $rescheduledDate = $this->timetable->rescheduleTaskAndRunInOneHour($task);
156 $this->timetable->incrementRetryCount($taskName);
157 $this->logger->info("Scheduler: '{task}' retry scheduled for {date}", ['task' => $taskName, 'date' => $rescheduledDate]);
158 }
159 $this->scheduleRetry = \false;
160 } else {
161 if ($this->timetable->getRetryCount($taskName) > 0) {
162 $this->timetable->clearRetryCount($taskName);
163 }
164 }
165 $executionResults[] = array('task' => $taskName, 'output' => $message);
166 }
167 $this->releaseLock();
168 }
169 }
170 $this->logger->info("done");
171 return $executionResults;
172 }
173 /**
174 * Run a specific task now. Will ignore the schedule completely.
175 *
176 * @param string $taskName
177 * @return string Task output.
178 */
179 public function runTaskNow($taskName)
180 {
181 $tasks = $this->loader->loadTasks();
182 foreach ($tasks as $task) {
183 if ($task->getName() === $taskName) {
184 if (!$this->acquireLockForTask($taskName, $task->getTTL())) {
185 return 'Execution skipped. Another process is currently executing this task.';
186 }
187 $result = $this->executeTask($task);
188 $this->releaseLock();
189 return $result;
190 }
191 }
192 throw new \InvalidArgumentException('Task ' . $taskName . ' not found');
193 }
194 /**
195 * Determines a task's scheduled time and persists it, overwriting the previous scheduled time.
196 *
197 * Call this method if your task's scheduled time has changed due to, for example, an option that
198 * was changed.
199 *
200 * @param Task $task Describes the scheduled task being rescheduled.
201 * @api
202 */
203 public function rescheduleTask(\Piwik\Scheduler\Task $task)
204 {
205 $this->logger->debug('Rescheduling task {task}', array('task' => $task->getName()));
206 $this->timetable->rescheduleTask($task);
207 }
208 /**
209 * Determines a task's scheduled time and persists it, overwriting the previous scheduled time.
210 *
211 * Call this method if your task's scheduled time has changed due to, for example, an option that
212 * was changed.
213 *
214 * The task will be run the first time tomorrow.
215 *
216 * @param Task $task Describes the scheduled task being rescheduled.
217 * @api
218 */
219 public function rescheduleTaskAndRunTomorrow(\Piwik\Scheduler\Task $task)
220 {
221 $this->logger->debug('Rescheduling task and setting first run for tomorrow {task}', array('task' => $task->getName()));
222 $this->timetable->rescheduleTaskAndRunTomorrow($task);
223 }
224 /**
225 * Returns true if the scheduler is currently running a task.
226 *
227 * @return bool
228 */
229 public function isRunningTask()
230 {
231 return $this->isRunningTask;
232 }
233 /**
234 * Return the next scheduled time given the class and method names of a scheduled task.
235 *
236 * @param string $className The name of the class that contains the scheduled task method.
237 * @param string $methodName The name of the scheduled task method.
238 * @param string|null $methodParameter Optional method parameter.
239 * @return mixed int|bool The time in milliseconds when the scheduled task will be executed
240 * next or false if it is not scheduled to run.
241 */
242 public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null)
243 {
244 return $this->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter);
245 }
246 /**
247 * Returns the list of the task names.
248 *
249 * @return string[]
250 */
251 public function getTaskList()
252 {
253 $tasks = $this->loader->loadTasks();
254 return array_map(function (\Piwik\Scheduler\Task $task) {
255 return $task->getName();
256 }, $tasks);
257 }
258 private function acquireLockForTask(string $taskName, int $ttlInSeconds) : bool
259 {
260 if (-1 === $ttlInSeconds) {
261 // lock disabled, so don't try to acquire one
262 return \true;
263 }
264 return $this->lock->acquireLock($taskName, $ttlInSeconds);
265 }
266 private function releaseLock()
267 {
268 $this->lock->unlock();
269 }
270 /**
271 * Executes the given task
272 *
273 * @param Task $task
274 * @return string
275 */
276 private function executeTask($task)
277 {
278 $this->logger->info("Scheduler: executing task {taskName}...", array('taskName' => $task->getName()));
279 $this->isRunningTask = \true;
280 $timer = new Timer();
281 /**
282 * Triggered directly before a scheduled task is executed
283 *
284 * @param Task $task The task that is about to be executed
285 */
286 Piwik::postEvent('ScheduledTasks.execute', array(&$task));
287 try {
288 $callable = array($task->getObjectInstance(), $task->getMethodName());
289 call_user_func($callable, $task->getMethodParameter());
290 $message = $timer->__toString();
291 } catch (\Exception $e) {
292 $this->logger->error("Scheduler: Error {errorMessage} for task '{task}'", ['errorMessage' => $e->getMessage(), 'task' => $task->getName()]);
293 $message = 'ERROR: ' . $e->getMessage();
294 // If the task has indicated that retrying on exception is safe then flag for rescheduling
295 if ($e instanceof \Piwik\Scheduler\RetryableException) {
296 $this->scheduleRetry = \true;
297 }
298 }
299 $this->isRunningTask = \false;
300 /**
301 * Triggered after a scheduled task is successfully executed.
302 *
303 * You can use the event to execute for example another task whenever a specific task is executed or to clean up
304 * certain resources.
305 *
306 * @param Task $task The task that was just executed
307 */
308 Piwik::postEvent('ScheduledTasks.execute.end', array(&$task));
309 $this->logger->info("Scheduler: finished. {timeElapsed}", array('timeElapsed' => $timer));
310 return $message;
311 }
312 }
313