3 namespace wcf\system\background
;
5 use wcf\data\user\User
;
6 use wcf\system\background\job\AbstractBackgroundJob
;
7 use wcf\system\background\job\AbstractUniqueBackgroundJob
;
8 use wcf\system\exception\ParentClassException
;
9 use wcf\system\session\SessionHandler
;
10 use wcf\system\SingletonFactory
;
14 * Manages the background queue.
16 * @author Tim Duesterhus
17 * @copyright 2001-2022 WoltLab GmbH
18 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
21 final class BackgroundQueueHandler
extends SingletonFactory
23 public const FORCE_CHECK_HTTP_HEADER_NAME
= 'woltlab-background-queue-check';
25 public const FORCE_CHECK_HTTP_HEADER_VALUE
= 'yes';
27 private bool $hasPendingCheck = false;
30 * Forces checking whether a background queue item is due.
31 * This means that the AJAX request to BackgroundQueuePerformAction is triggered.
33 public function forceCheck(): void
35 WCF
::getSession()->register('forceBackgroundQueuePerform', true);
37 WCF
::getTPL()->assign([
38 'forceBackgroundQueuePerform' => true,
41 $this->hasPendingCheck
= true;
45 * Enqueues the given job(s) for execution in the specified number of
46 * seconds. Defaults to "as soon as possible" (0 seconds).
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()
52 public function enqueueIn(AbstractBackgroundJob|
array $jobs, int $time = 0): void
54 $this->enqueueAt($jobs, TIME_NOW +
$time);
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!
62 * @param AbstractBackgroundJob|AbstractBackgroundJob[] $jobs
63 * @param $time Earliest time to consider the job for execution.
64 * @throws \InvalidArgumentException
66 public function enqueueAt(AbstractBackgroundJob|
array $jobs, int $time): void
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
. ").");
71 if (!\
is_array($jobs)) {
75 foreach ($jobs as $job) {
76 if (!($job instanceof AbstractBackgroundJob
)) {
77 throw new ParentClassException(\
get_class($job), AbstractBackgroundJob
::class);
83 WCF
::getDB()->beginTransaction();
84 $sql = "INSERT INTO wcf1_background_job
85 (job, time,identifier)
87 $statement = WCF
::getDB()->prepare($sql);
89 FROM wcf1_background_job
92 $selectJobStatement = WCF
::getDB()->prepare($sql);
94 foreach ($jobs as $job) {
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) {
103 $identifier = $job->identifier();
106 $statement->execute([
112 WCF
::getDB()->commitTransaction();
116 WCF
::getDB()->rollBackTransaction();
122 * Immediately performs the given job.
123 * This method automatically handles requeuing in case of failure.
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
130 * @param $debugSynchronousExecution Disables fail-safe mechanisms, errors will no longer be suppressed.
133 public function performJob(AbstractBackgroundJob
$job, bool $debugSynchronousExecution = false): void
135 $user = WCF
::getUser();
138 SessionHandler
::getInstance()->changeUser(new User(null), true);
139 if (!WCF
::debugModeIsEnabled()) {
143 } catch (\Throwable
$e) {
144 // do not suppress exceptions for debugging purposes, see https://github.com/WoltLab/WCF/issues/2501
145 if ($debugSynchronousExecution) {
151 if ($job->getFailures() <= $job::MAX_FAILURES
) {
152 $this->enqueueIn($job, $job->retryAfter());
154 if (WCF
::debugModeIsEnabled()) {
155 \wcf\functions\exception\
logThrowable($e);
158 $job->onFinalFailure();
160 // job failed too often: log
161 \wcf\functions\exception\
logThrowable($e);
164 if (!WCF
::debugModeIsEnabled()) {
167 SessionHandler
::getInstance()->changeUser($user, true);
172 * Performs the (single) job that is due next.
173 * This method automatically handles requeuing in case of failure.
175 * @return bool true if this call attempted to execute a job regardless of its result
177 public function performNextJob(): bool
179 WCF
::getDB()->beginTransaction();
182 $sql = "SELECT jobID, job
183 FROM wcf1_background_job
186 ORDER BY time ASC, jobID ASC
188 $statement = WCF
::getDB()->prepare($sql, 1);
189 $statement->execute([
193 $row = $statement->fetchSingleRow();
195 // nothing to do here
200 $sql = "UPDATE wcf1_background_job
205 $statement = WCF
::getDB()->prepare($sql);
206 $statement->execute([
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
218 WCF
::getDB()->commitTransaction();
222 WCF
::getDB()->rollBackTransaction();
228 // no shut up operator, exception will be caught
229 $job = \
unserialize($row['job']);
231 $this->performJob($job);
233 } catch (\Throwable
$e) {
234 // job is completely broken: log
235 \wcf\functions\exception\
logThrowable($e);
237 // remove entry of processed job
238 $sql = "DELETE FROM wcf1_background_job
240 $statement = WCF
::getDB()->prepare($sql);
241 $statement->execute([$row['jobID']]);
243 if ($job instanceof AbstractUniqueBackgroundJob
&& $job->queueAgain()) {
244 $this->enqueueIn($job->newInstance(), $job->retryAfter());
251 * Returns how many items are due.
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.
257 public function getRunnableCount(): int
259 $sql = "SELECT COUNT(*)
260 FROM wcf1_background_job
263 $statement = WCF
::getDB()->prepare($sql);
264 $statement->execute(['ready', TIME_NOW
]);
266 return $statement->fetchSingleColumn();
270 * Indicates that the client should trigger a check for
271 * pending jobs in the background queue.
275 public function hasPendingCheck(): bool
277 return $this->hasPendingCheck
;