Commit | Line | Data |
---|---|---|
db6698ad TD |
1 | <?php |
2 | namespace wcf\system\background; | |
6b4fb98a | 3 | use wcf\data\user\User; |
db6698ad | 4 | use wcf\system\background\job\AbstractBackgroundJob; |
fc332b27 | 5 | use wcf\system\exception\SystemException; |
4205bd62 | 6 | use wcf\system\session\SessionHandler; |
db6698ad TD |
7 | use wcf\system\SingletonFactory; |
8 | use wcf\system\WCF; | |
9 | ||
10 | /** | |
11 | * Manages the background queue. | |
a5f8fd9f | 12 | * |
db6698ad TD |
13 | * @author Tim Duesterhus |
14 | * @copyright 2001-2015 WoltLab GmbH | |
15 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
16 | * @package com.woltlab.wcf | |
17 | * @subpackage system.background.job | |
18 | * @category Community Framework | |
63b02e3f | 19 | * @since 2.2 |
db6698ad TD |
20 | */ |
21 | class BackgroundQueueHandler extends SingletonFactory { | |
27f8a148 TD |
22 | /** |
23 | * Forces checking whether a background queue item is due. | |
24 | * This means that the AJAX request to BackgroundQueuePerformAction is triggered. | |
25 | */ | |
26 | public function forceCheck() { | |
27 | WCF::getTPL()->assign([ | |
28 | 'forceBackgroundQueuePerform' => true | |
29 | ]); | |
30 | } | |
31 | ||
fc332b27 | 32 | /** |
70f9375b | 33 | * Enqueues the given job(s) for execution in the specified number of |
fc332b27 TD |
34 | * seconds. Defaults to "as soon as possible" (0 seconds). |
35 | * | |
70f9375b TD |
36 | * @param mixed $jobs Either an instance of \wcf\system\background\job\AbstractBackgroundJob or an array of these |
37 | * @param int $time Minimum number of seconds to wait before performing the job. | |
fc332b27 TD |
38 | * @see \wcf\system\background\BackgroundQueueHandler::enqueueAt() |
39 | */ | |
70f9375b TD |
40 | public function enqueueIn($jobs, $time = 0) { |
41 | return self::enqueueAt($jobs, TIME_NOW + $time); | |
fc332b27 TD |
42 | } |
43 | ||
db6698ad | 44 | /** |
70f9375b | 45 | * Enqueues the given job(s) for execution at the given time. |
db6698ad TD |
46 | * Note: The time is a minimum time. Depending on the size of |
47 | * the queue the job can be performed later as well! | |
a5f8fd9f | 48 | * |
70f9375b TD |
49 | * @param mixed $jobs Either an instance of \wcf\system\background\job\AbstractBackgroundJob or an array of these |
50 | * @param int $time Earliest time to consider the job for execution. | |
db6698ad | 51 | */ |
70f9375b | 52 | public function enqueueAt($jobs, $time) { |
fc332b27 TD |
53 | if ($time < TIME_NOW) { |
54 | throw new SystemException("You may not schedule a job in the past (".$time." is smaller than the current timestamp ".TIME_NOW.")."); | |
55 | } | |
70f9375b TD |
56 | if (!is_array($jobs)) $jobs = [ $jobs ]; |
57 | foreach ($jobs as $job) { | |
58 | if (!($job instanceof AbstractBackgroundJob)) { | |
59 | throw new SystemException('$jobs contains an item that does not extend \wcf\system\background\job\AbstractBackgroundJob.'); | |
60 | } | |
61 | } | |
fc332b27 | 62 | |
70f9375b | 63 | WCF::getDB()->beginTransaction(); |
db6698ad TD |
64 | $sql = "INSERT INTO wcf".WCF_N."_background_job |
65 | (job, time) | |
66 | VALUES (?, ?)"; | |
67 | $statement = WCF::getDB()->prepareStatement($sql); | |
70f9375b TD |
68 | foreach ($jobs as $job) { |
69 | $statement->execute([ | |
70 | serialize($job), | |
71 | $time | |
72 | ]); | |
73 | } | |
74 | WCF::getDB()->commitTransaction(); | |
db6698ad | 75 | } |
3ea5d8d7 TD |
76 | |
77 | /** | |
78 | * Immediatly performs the given job. | |
79 | * This method automatically handles requeuing in case of failure. | |
80 | * | |
81 | * This method is used internally by performNextJob(), but it can | |
82 | * be useful if you wish immediate execution of a certain job, but | |
83 | * don't want to miss the automated error handling mechanism of the | |
84 | * queue. | |
85 | * | |
86 | * @param \wcf\system\background\job\AbstractBackgroundJob $job The job to perform. | |
87 | */ | |
88 | public function performJob(AbstractBackgroundJob $job) { | |
6b4fb98a TD |
89 | $user = WCF::getUser(); |
90 | ||
3ea5d8d7 | 91 | try { |
6b4fb98a | 92 | SessionHandler::getInstance()->changeUser(new User(null), true); |
3ea5d8d7 TD |
93 | $job->perform(); |
94 | } | |
dc072501 TD |
95 | catch (\Throwable $e) { |
96 | // gotta catch 'em all | |
97 | $job->fail(); | |
98 | ||
99 | if ($job->getFailures() <= $job::MAX_FAILURES) { | |
100 | $this->enqueueIn($job, $job->retryAfter()); | |
101 | } | |
102 | else { | |
103 | // job failed too often: log | |
104 | \wcf\functions\exception\logThrowable($e); | |
105 | } | |
106 | } | |
3ea5d8d7 TD |
107 | catch (\Exception $e) { |
108 | // gotta catch 'em all | |
109 | $job->fail(); | |
110 | ||
111 | if ($job->getFailures() <= $job::MAX_FAILURES) { | |
fc332b27 | 112 | $this->enqueueIn($job, $job->retryAfter()); |
3ea5d8d7 TD |
113 | } |
114 | else { | |
115 | // job failed too often: log | |
280b49db | 116 | \wcf\functions\exception\logThrowable($e); |
3ea5d8d7 TD |
117 | } |
118 | } | |
6b4fb98a TD |
119 | finally { |
120 | SessionHandler::getInstance()->changeUser($user, true); | |
121 | } | |
3ea5d8d7 TD |
122 | } |
123 | ||
db6698ad TD |
124 | /** |
125 | * Performs the (single) job that is due next. | |
126 | * This method automatically handles requeuing in case of failure. | |
127 | */ | |
3ea5d8d7 | 128 | public function performNextJob() { |
db6698ad TD |
129 | WCF::getDB()->beginTransaction(); |
130 | $commited = false; | |
131 | try { | |
132 | $sql = "SELECT jobID, job | |
133 | FROM wcf".WCF_N."_background_job | |
134 | WHERE status = ? | |
135 | AND time <= ? | |
136 | ORDER BY time ASC, jobID ASC | |
137 | FOR UPDATE"; | |
138 | $statement = WCF::getDB()->prepareStatement($sql, 1); | |
139 | $statement->execute([ | |
140 | 'ready', | |
141 | TIME_NOW | |
142 | ]); | |
143 | $row = $statement->fetchSingleRow(); | |
144 | if (!$row) { | |
145 | // nothing to do here | |
146 | return; | |
147 | } | |
148 | ||
149 | // lock job | |
150 | $sql = "UPDATE wcf".WCF_N."_background_job | |
151 | SET status = ?, | |
152 | time = ? | |
153 | WHERE jobID = ? | |
154 | AND status = ?"; | |
155 | $statement = WCF::getDB()->prepareStatement($sql); | |
156 | $statement->execute([ | |
157 | 'processing', | |
158 | TIME_NOW, | |
159 | $row['jobID'], | |
160 | 'ready' | |
161 | ]); | |
162 | if ($statement->getAffectedRows() != 1) { | |
163 | // somebody stole the job | |
164 | // this cannot happen unless MySQL violates it's contract to lock the row | |
165 | // -> silently ignore, there will be plenty of other oppurtunities to perform a job | |
166 | return; | |
167 | } | |
168 | WCF::getDB()->commitTransaction(); | |
169 | $commited = true; | |
170 | } | |
171 | finally { | |
172 | if (!$commited) WCF::getDB()->rollbackTransaction(); | |
173 | } | |
a5f8fd9f | 174 | |
db6698ad TD |
175 | $job = null; |
176 | try { | |
177 | // no shut up operator, exception will be caught | |
178 | $job = unserialize($row['job']); | |
179 | if ($job) { | |
3ea5d8d7 | 180 | $this->performJob($job); |
db6698ad TD |
181 | } |
182 | } | |
dc072501 TD |
183 | catch (\Throwable $e) { |
184 | // job is completely broken: log | |
185 | \wcf\functions\exception\logThrowable($e); | |
186 | } | |
db6698ad | 187 | catch (\Exception $e) { |
3ea5d8d7 | 188 | // job is completely broken: log |
280b49db | 189 | \wcf\functions\exception\logThrowable($e); |
db6698ad TD |
190 | } |
191 | finally { | |
192 | // remove entry of processed job | |
193 | $sql = "DELETE FROM wcf".WCF_N."_background_job | |
194 | WHERE jobID = ?"; | |
195 | $statement = WCF::getDB()->prepareStatement($sql); | |
196 | $statement->execute([ $row['jobID'] ]); | |
197 | } | |
198 | } | |
72db0118 TD |
199 | |
200 | /** | |
201 | * Returns how many items are due. | |
202 | * Note: Do not rely on the return value being correct, some other process may | |
203 | * have modified the queue contents, before this method returns. Think of it as an | |
204 | * approximation to know whether you should spend some time to clear the queue. | |
205 | * | |
206 | * @return int | |
207 | */ | |
208 | public function getRunnableCount() { | |
209 | $sql = "SELECT COUNT(*) | |
210 | FROM wcf".WCF_N."_background_job | |
211 | WHERE status = ? | |
212 | AND time <= ?"; | |
213 | $statement = WCF::getDB()->prepareStatement($sql); | |
214 | $statement->execute([ 'ready', TIME_NOW ]); | |
215 | ||
216 | return $statement->fetchSingleColumn(); | |
217 | } | |
6032b6a7 | 218 | } |