@@ -90,16 +90,18 @@ public function next(string $groupName): ?ScheduledJob
9090 $ claim = Algorithms::generateUUID ();
9191 $ tableName = ScheduledJob::TABLE_NAME ;
9292
93- $ update =
93+ $ claimQuery =
9494 /**
95- * Step 1: Create a derived table using the "idx_for_update" index
96- * that only contains one row.
97- * Step 2: Join that row against the actual job to be claimed on
98- * the primary key column.
99- * Step 3: UPDATE that row with the claim value.
95+ * Step 1: Insert the "claimed" value without locking.
96+ *
97+ * Step 1.1: Create a derived table using the "idx_for_update" index
98+ * that only contains one row.
99+ * Step 1.2: Join that row against the actual job to be claimed on
100+ * the primary key column.
101+ * Step 1.3: UPDATE that row with the claim value.
100102 *
101103 * Otherwise, MySQL would use the "idx_groupname" index, fetch
102- * millions of rows, use a temp table to sort those millions
104+ * millions of rows, use a temp table to sort those millions,
103105 * and limit the result to the one row to be claimed.
104106 *
105107 * @lang MySQL
@@ -116,7 +118,7 @@ public function next(string $groupName): ?ScheduledJob
116118 INNER JOIN {$ tableName }
117119 USING (identifier)
118120 SET claimed = :claimed,
119- running = 1 ,
121+ running = 2 ,
120122 activity = NOW()
121123 WHERE claimed = ""
122124 MySQL ;
@@ -128,7 +130,7 @@ public function next(string $groupName): ?ScheduledJob
128130 ->onExceptionsOfType (RetryableException::class)
129131 ->task (fn () => $ this ->dbal
130132 ->executeQuery (
131- $ update ,
133+ $ claimQuery ,
132134 [
133135 'now ' => $ this ->timeBaseForDueDateCalculation ->getNow (),
134136 'groupname ' => $ groupName ,
@@ -141,15 +143,21 @@ public function next(string $groupName): ?ScheduledJob
141143 ]
142144 ));
143145
144- $ select = /** @lang MySQL */ <<<MySQL
146+ $ selectQuery =
147+ /**
148+ * Step 2: Find the row in the database.
149+ *
150+ * @lang MySQL
151+ */
152+ <<<"MySQL"
145153 SELECT identifier, duedate, queue, job, incarnation, claimed, running
146154 FROM {$ tableName }
147155 WHERE claimed = :claimed
148156 AND groupname = :groupname
149157 MySQL ;
150158 $ row = $ this ->dbal
151159 ->executeQuery (
152- $ select ,
160+ $ selectQuery ,
153161 [
154162 'groupname ' => $ groupName ,
155163 'claimed ' => $ claim ,
@@ -165,6 +173,41 @@ public function next(string $groupName): ?ScheduledJob
165173 return null ;
166174 }
167175
176+ $ releaseQuery =
177+ /**
178+ * Step 3: Unlock the row and allow parallel processes to overwrite the "claimed" value
179+ *
180+ * @lang MySQL
181+ */
182+ <<<"MySQL"
183+ UPDATE {$ tableName }
184+ SET running = 1,
185+ activity = NOW()
186+ WHERE claimed = :claimed
187+ AND groupname = :groupname
188+ AND running = 2
189+ MySQL ;
190+ (new Retry ())
191+ /**
192+ * @see http://backoffcalculator.com/?attempts=5&rate=1&interval=0.5
193+ */
194+ ->withExponentialBackoff (retryInterval: 0.5 , maxRetries: 5 )
195+ ->onExceptionsOfType (RetryableException::class)
196+ ->task (fn () => $ this ->dbal
197+ ->executeQuery (
198+ $ releaseQuery ,
199+ [
200+ 'groupname ' => $ groupName ,
201+ 'claimed ' => $ claim ,
202+ ],
203+ [
204+ 'groupname ' => Types::STRING ,
205+ 'claimed ' => Types::STRING ,
206+ ]
207+ ));
208+
209+
210+
168211 return ScheduledJob::createInternal (
169212 job: $ row ['job ' ],
170213 queue: $ row ['queue ' ],
@@ -232,7 +275,6 @@ public function fail(ScheduledJob $job, string $reason): void
232275 running = 0,
233276 activity = NOW()
234277 WHERE identifier = :identifier
235- AND claimed = :claimed
236278 LIMIT 1
237279 MySQL ;
238280 $ this ->dbal
@@ -259,19 +301,16 @@ public function activity(ScheduledJob $job): void
259301 UPDATE {$ tableName }
260302 SET activity = NOW()
261303 WHERE identifier = :identifier
262- AND claimed = :claimed
263304 LIMIT 1
264305 MySQL ;
265306 $ this ->dbal
266307 ->executeQuery (
267308 $ update ,
268309 [
269310 'identifier ' => $ job ->getIdentifier (),
270- 'claimed ' => $ job ->getClaimed (),
271311 ],
272312 [
273313 'identifier ' => Types::STRING ,
274- 'claimed ' => Types::STRING ,
275314 ]
276315 );
277316 }
@@ -281,27 +320,52 @@ protected function scheduleJob(ScheduledJob $job): void
281320 $ this ->validateGroupName ($ job ->getGroupName ());
282321 $ tableName = ScheduledJob::TABLE_NAME ;
283322
284- $ statement = /** @lang MySQL */ <<<MySQL
323+ $ statement =
324+ /**
325+ * `running = 0`:
326+ *
327+ * - Meaning: The existing job is currently pending.
328+ * - Set claimed to empty, which should be the case anyway.
329+ * - Use the lesser due date to avoid pushing jobs further and further into the future
330+ *
331+ * `running = 1`:
332+ *
333+ * - Meaning: The existing job is currently running.
334+ * - Set claimed to empty to cause a re-run, once the current run finishes.
335+ * - Use the upcoming due date, the current job is running anyway.
336+ *
337+ * `running = 2`:
338+ *
339+ * - Meaning: The existing job in its warmup phase.
340+ * - Keeping claimed "as is" is mandatory for the current run to pick up.
341+ * - The due date doesn't matter because once finished, the current run will vanish.
342+ *
343+ * @lang MySQL
344+ */ <<<MySQL
285345 INSERT INTO {$ tableName }
286346 (groupname, identifier, duedate, activity, queue, job, incarnation, claimed, running)
287347 VALUES (:groupname, :identifier, :duedate, NOW(), :queue, :job, :incarnation, :claimed, :running)
288348 ON DUPLICATE KEY
289349 UPDATE
290- duedate = CASE
350+ duedate = CASE
351+ WHEN running = 0
352+ THEN IF(duedate < :duedate, duedate, :duedate)
291353 WHEN running = 1
292- -- If the job is already running, this is "another one",
293- -- so schedule the next one according to its own date.
294354 THEN :duedate
295- WHEN duedate < :duedate
296- -- If this reschedules a waiting job, use the lower value
355+ WHEN running = 2
297356 THEN duedate
298- ELSE
299- :duedate
300357 END,
301358 incarnation = :incarnation,
302- queue = :queue,
303- job = :job,
304- claimed = :claimed
359+ queue = :queue,
360+ job = :job,
361+ claimed = CASE
362+ WHEN running = 0
363+ THEN :claimed
364+ WHEN running = 1
365+ THEN :claimed
366+ WHEN running = 2
367+ THEN claimed
368+ END
305369 MySQL ;
306370
307371 (new Retry ())
0 commit comments