The function `getContent()` is called by `getData()` and internally `getContent(...
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / background / BackgroundQueueHandler.class.php
... / ...
CommitLineData
1<?php
2
3namespace wcf\system\background;
4
5use wcf\data\user\User;
6use wcf\system\background\job\AbstractBackgroundJob;
7use wcf\system\background\job\AbstractUniqueBackgroundJob;
8use wcf\system\exception\ParentClassException;
9use wcf\system\session\SessionHandler;
10use wcf\system\SingletonFactory;
11use wcf\system\WCF;
12
13/**
14 * Manages the background queue.
15 *
16 * @author Tim Duesterhus
17 * @copyright 2001-2022 WoltLab GmbH
18 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
19 * @since 3.0
20 */
21final class BackgroundQueueHandler extends SingletonFactory
22{
23 public const FORCE_CHECK_HTTP_HEADER_NAME = 'woltlab-background-queue-check';
24
25 public const FORCE_CHECK_HTTP_HEADER_VALUE = 'yes';
26
27 private bool $hasPendingCheck = false;
28
29 /**
30 * Forces checking whether a background queue item is due.
31 * This means that the AJAX request to BackgroundQueuePerformAction is triggered.
32 */
33 public function forceCheck(): void
34 {
35 WCF::getSession()->register('forceBackgroundQueuePerform', true);
36
37 WCF::getTPL()->assign([
38 'forceBackgroundQueuePerform' => true,
39 ]);
40
41 $this->hasPendingCheck = true;
42 }
43
44 /**
45 * Enqueues the given job(s) for execution in the specified number of
46 * seconds. Defaults to "as soon as possible" (0 seconds).
47 *
48 * @param AbstractBackgroundJob|AbstractBackgroundJob[] $jobs
49 * @param $time Minimum number of seconds to wait before performing the job.
50 * @see \wcf\system\background\BackgroundQueueHandler::enqueueAt()
51 */
52 public function enqueueIn(AbstractBackgroundJob|array $jobs, int $time = 0): void
53 {
54 $this->enqueueAt($jobs, TIME_NOW + $time);
55 }
56
57 /**
58 * Enqueues the given job(s) for execution at the given time.
59 * Note: The time is a minimum time. Depending on the size of
60 * the queue the job can be performed later as well!
61 *
62 * @param AbstractBackgroundJob|AbstractBackgroundJob[] $jobs
63 * @param $time Earliest time to consider the job for execution.
64 * @throws \InvalidArgumentException
65 */
66 public function enqueueAt(AbstractBackgroundJob|array $jobs, int $time): void
67 {
68 if ($time < TIME_NOW) {
69 throw new \InvalidArgumentException("You may not schedule a job in the past (" . $time . " is smaller than the current timestamp " . TIME_NOW . ").");
70 }
71 if (!\is_array($jobs)) {
72 $jobs = [$jobs];
73 }
74
75 foreach ($jobs as $job) {
76 if (!($job instanceof AbstractBackgroundJob)) {
77 throw new ParentClassException(\get_class($job), AbstractBackgroundJob::class);
78 }
79 }
80
81 $committed = false;
82 try {
83 WCF::getDB()->beginTransaction();
84 $sql = "INSERT INTO wcf1_background_job
85 (job, time,identifier)
86 VALUES (?, ?, ?)";
87 $statement = WCF::getDB()->prepare($sql);
88 $sql = "SELECT jobID
89 FROM wcf1_background_job
90 WHERE identifier = ?
91 FOR UPDATE";
92 $selectJobStatement = WCF::getDB()->prepare($sql);
93
94 foreach ($jobs as $job) {
95 $identifier = null;
96 if ($job instanceof AbstractUniqueBackgroundJob) {
97 // Check if the job is already in the queue
98 $selectJobStatement->execute([$job->identifier()]);
99 $jobID = $selectJobStatement->fetchSingleColumn();
100 if ($jobID !== false) {
101 continue;
102 }
103 $identifier = $job->identifier();
104 }
105
106 $statement->execute([
107 \serialize($job),
108 $time,
109 $identifier
110 ]);
111 }
112 WCF::getDB()->commitTransaction();
113 $committed = true;
114 } finally {
115 if (!$committed) {
116 WCF::getDB()->rollBackTransaction();
117 }
118 }
119 }
120
121 /**
122 * Immediately performs the given job.
123 * This method automatically handles requeuing in case of failure.
124 *
125 * This method is used internally by performNextJob(), but it can
126 * be useful if you wish immediate execution of a certain job, but
127 * don't want to miss the automated error handling mechanism of the
128 * queue.
129 *
130 * @param $debugSynchronousExecution Disables fail-safe mechanisms, errors will no longer be suppressed.
131 * @throws \Throwable
132 */
133 public function performJob(AbstractBackgroundJob $job, bool $debugSynchronousExecution = false): void
134 {
135 $user = WCF::getUser();
136
137 try {
138 SessionHandler::getInstance()->changeUser(new User(null), true);
139 if (!WCF::debugModeIsEnabled()) {
140 \ob_start();
141 }
142 $job->perform();
143 } catch (\Throwable $e) {
144 // do not suppress exceptions for debugging purposes, see https://github.com/WoltLab/WCF/issues/2501
145 if ($debugSynchronousExecution) {
146 throw $e;
147 }
148
149 $job->fail();
150
151 if ($job->getFailures() <= $job::MAX_FAILURES) {
152 $this->enqueueIn($job, $job->retryAfter());
153
154 if (WCF::debugModeIsEnabled()) {
155 \wcf\functions\exception\logThrowable($e);
156 }
157 } else {
158 $job->onFinalFailure();
159
160 // job failed too often: log
161 \wcf\functions\exception\logThrowable($e);
162 }
163 } finally {
164 if (!WCF::debugModeIsEnabled()) {
165 \ob_end_clean();
166 }
167 SessionHandler::getInstance()->changeUser($user, true);
168 }
169 }
170
171 /**
172 * Performs the (single) job that is due next.
173 * This method automatically handles requeuing in case of failure.
174 *
175 * @return bool true if this call attempted to execute a job regardless of its result
176 */
177 public function performNextJob(): bool
178 {
179 WCF::getDB()->beginTransaction();
180 $committed = false;
181 try {
182 $sql = "SELECT jobID, job
183 FROM wcf1_background_job
184 WHERE status = ?
185 AND time <= ?
186 ORDER BY time ASC, jobID ASC
187 FOR UPDATE";
188 $statement = WCF::getDB()->prepare($sql, 1);
189 $statement->execute([
190 'ready',
191 TIME_NOW,
192 ]);
193 $row = $statement->fetchSingleRow();
194 if (!$row) {
195 // nothing to do here
196 return false;
197 }
198
199 // lock job
200 $sql = "UPDATE wcf1_background_job
201 SET status = ?,
202 time = ?
203 WHERE jobID = ?
204 AND status = ?";
205 $statement = WCF::getDB()->prepare($sql);
206 $statement->execute([
207 'processing',
208 TIME_NOW,
209 $row['jobID'],
210 'ready',
211 ]);
212 if ($statement->getAffectedRows() != 1) {
213 // somebody stole the job
214 // this cannot happen unless MySQL violates it's contract to lock the row
215 // -> silently ignore, there will be plenty of other opportunities to perform a job
216 return true;
217 }
218 WCF::getDB()->commitTransaction();
219 $committed = true;
220 } finally {
221 if (!$committed) {
222 WCF::getDB()->rollBackTransaction();
223 }
224 }
225
226 $job = null;
227 try {
228 // no shut up operator, exception will be caught
229 $job = \unserialize($row['job']);
230 if ($job) {
231 $this->performJob($job);
232 }
233 } catch (\Throwable $e) {
234 // job is completely broken: log
235 \wcf\functions\exception\logThrowable($e);
236 } finally {
237 // remove entry of processed job
238 $sql = "DELETE FROM wcf1_background_job
239 WHERE jobID = ?";
240 $statement = WCF::getDB()->prepare($sql);
241 $statement->execute([$row['jobID']]);
242 }
243 if ($job instanceof AbstractUniqueBackgroundJob && $job->queueAgain()) {
244 $this->enqueueIn($job->newInstance(), $job->retryAfter());
245 }
246
247 return true;
248 }
249
250 /**
251 * Returns how many items are due.
252 *
253 * Note: Do not rely on the return value being correct, some other process may
254 * have modified the queue contents, before this method returns. Think of it as an
255 * approximation to know whether you should spend some time to clear the queue.
256 */
257 public function getRunnableCount(): int
258 {
259 $sql = "SELECT COUNT(*)
260 FROM wcf1_background_job
261 WHERE status = ?
262 AND time <= ?";
263 $statement = WCF::getDB()->prepare($sql);
264 $statement->execute(['ready', TIME_NOW]);
265
266 return $statement->fetchSingleColumn();
267 }
268
269 /**
270 * Indicates that the client should trigger a check for
271 * pending jobs in the background queue.
272 *
273 * @since 6.0
274 */
275 public function hasPendingCheck(): bool
276 {
277 return $this->hasPendingCheck;
278 }
279}