Skip to content

Commit 7a397d2

Browse files
committed
Adds retry delay and retry until functionality
Adds `retryWithDelay` and `retryUntil` methods to the `Queueable` trait. These methods allow developers to define custom retry logic for queued jobs, improving resilience and control over job execution. Also introduces corresponding properties to store the configuration. Removes redundant comment Removes a comment in `QueueableTest.php` that is no longer necessary after previous changes. Allows retrieval of retryUntil value Allows retrieving the current retryUntil value when no arguments are passed to the retryUntil method. This is useful for inspecting the retryUntil value that has been set on a job.
1 parent 23c7895 commit 7a397d2

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed

src/Illuminate/Bus/Queueable.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ trait Queueable
4949
*/
5050
public $delay;
5151

52+
/**
53+
* The number of seconds to wait before retrying a job that encountered an uncaught exception.
54+
*
55+
* @var \DateTimeInterface|\DateInterval|array|int|null
56+
*/
57+
public $backoff;
58+
59+
/**
60+
* The timestamp indicating when the job should timeout.
61+
*
62+
* @var \DateTimeInterface|int|null
63+
*/
64+
public $retryUntil;
65+
5266
/**
5367
* Indicates whether the job should be dispatched after all database transactions have committed.
5468
*
@@ -206,6 +220,36 @@ public function withoutDelay()
206220
return $this;
207221
}
208222

223+
/**
224+
* Set the number of seconds to wait before retrying a job that encountered an uncaught exception.
225+
*
226+
* @param \DateTimeInterface|\DateInterval|array|int $delays
227+
* @return $this
228+
*/
229+
public function retryWithDelay($delays)
230+
{
231+
$this->backoff = $delays;
232+
233+
return $this;
234+
}
235+
236+
/**
237+
* Set the timestamp indicating when the job should timeout.
238+
*
239+
* @param \DateTimeInterface|int|null $datetime
240+
* @return $this|\DateTimeInterface|int|null
241+
*/
242+
public function retryUntil($datetime = null)
243+
{
244+
if (func_num_args() === 0) {
245+
return $this->retryUntil;
246+
}
247+
248+
$this->retryUntil = $datetime;
249+
250+
return $this;
251+
}
252+
209253
/**
210254
* Indicate that the job should be dispatched after all database transactions have committed.
211255
*

tests/Bus/QueueableTest.php

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace Illuminate\Tests\Bus;
44

5+
use Carbon\Carbon;
56
use Illuminate\Bus\Queueable;
7+
use Illuminate\Container\Container;
8+
use Illuminate\Queue\SyncQueue;
69
use PHPUnit\Framework\Attributes\DataProvider;
710
use PHPUnit\Framework\TestCase;
811

@@ -65,6 +68,205 @@ public function testAllOnQueue(mixed $queue, ?string $expected): void
6568
$this->assertSame($job->queue, $expected);
6669
$this->assertSame($job->chainQueue, $expected);
6770
}
71+
72+
public static function retryWithDelayDataProvider(): array
73+
{
74+
$datetime = Carbon::now()->addHour();
75+
$interval = new \DateInterval('PT1H');
76+
77+
return [
78+
'integer' => [60, 60],
79+
'array of integers' => [[10, 30, 60, 120], [10, 30, 60, 120]],
80+
'DateTimeInterface' => [$datetime, $datetime],
81+
'DateInterval' => [$interval, $interval],
82+
];
83+
}
84+
85+
#[DataProvider('retryWithDelayDataProvider')]
86+
public function testRetryWithDelay(mixed $delays, mixed $expected): void
87+
{
88+
$job = new FakeJob();
89+
$result = $job->retryWithDelay($delays);
90+
91+
if ($expected instanceof \DateTimeInterface && $job->backoff instanceof \DateTimeInterface) {
92+
$this->assertEquals($expected->getTimestamp(), $job->backoff->getTimestamp());
93+
} elseif ($expected instanceof \DateInterval && $job->backoff instanceof \DateInterval) {
94+
$this->assertEquals($expected->format('%R%Y%M%D%H%I%S'), $job->backoff->format('%R%Y%M%D%H%I%S'));
95+
} else {
96+
$this->assertSame($job->backoff, $expected);
97+
}
98+
$this->assertSame($result, $job);
99+
}
100+
101+
public static function retryUntilDataProvider(): array
102+
{
103+
$datetime = Carbon::now()->addDay();
104+
$timestamp = $datetime->getTimestamp();
105+
106+
return [
107+
'DateTimeInterface' => [$datetime, $datetime],
108+
'timestamp' => [$timestamp, $timestamp],
109+
];
110+
}
111+
112+
#[DataProvider('retryUntilDataProvider')]
113+
public function testRetryUntil(mixed $datetime, mixed $expected): void
114+
{
115+
$job = new FakeJob();
116+
$result = $job->retryUntil($datetime);
117+
118+
$this->assertSame($job->retryUntil, $expected);
119+
$this->assertSame($result, $job);
120+
}
121+
122+
public function testRetryWithDelayReturnsSelfForFluentInterface(): void
123+
{
124+
$job = new FakeJob();
125+
$result = $job->retryWithDelay(60);
126+
127+
$this->assertSame($job, $result);
128+
}
129+
130+
public function testRetryUntilReturnsSelfForFluentInterface(): void
131+
{
132+
$job = new FakeJob();
133+
$result = $job->retryUntil(Carbon::now());
134+
135+
$this->assertSame($job, $result);
136+
}
137+
138+
public function testRetryWithDelayAndRetryUntilCanBeChained(): void
139+
{
140+
$job = new FakeJob();
141+
$datetime = Carbon::now()->addDay();
142+
$delays = [10, 30, 60];
143+
144+
$result = $job->retryWithDelay($delays)->retryUntil($datetime);
145+
146+
$this->assertSame($job->backoff, $delays);
147+
$this->assertSame($job->retryUntil, $datetime);
148+
$this->assertSame($result, $job);
149+
}
150+
151+
public function testRetryWithDelayCanBeChainedWithOtherMethods(): void
152+
{
153+
$job = new FakeJob();
154+
155+
$result = $job->retryWithDelay(60)
156+
->onQueue('high')
157+
->onConnection('redis');
158+
159+
$this->assertSame($job->backoff, 60);
160+
$this->assertSame($job->queue, 'high');
161+
$this->assertSame($job->connection, 'redis');
162+
$this->assertSame($result, $job);
163+
}
164+
165+
public function testRetryUntilCanBeChainedWithOtherMethods(): void
166+
{
167+
$job = new FakeJob();
168+
$datetime = Carbon::now()->addDay();
169+
170+
$result = $job->retryUntil($datetime)
171+
->onQueue('high')
172+
->delay(30);
173+
174+
$this->assertSame($job->retryUntil, $datetime);
175+
$this->assertSame($job->queue, 'high');
176+
$this->assertSame($job->delay, 30);
177+
$this->assertSame($result, $job);
178+
}
179+
180+
public function testRetryWithDelayPropertyIsUsedInQueuePayload(): void
181+
{
182+
$job = new FakeJob();
183+
$job->retryWithDelay([10, 30, 60]);
184+
185+
$queue = new SyncQueue(new Container);
186+
$queue->setContainer(new Container);
187+
188+
$reflection = new \ReflectionClass($queue);
189+
$method = $reflection->getMethod('getJobBackoff');
190+
$method->setAccessible(true);
191+
192+
$backoff = $method->invoke($queue, $job);
193+
194+
$this->assertSame('10,30,60', $backoff);
195+
}
196+
197+
public function testRetryWithDelayWithSingleIntegerIsUsedInQueuePayload(): void
198+
{
199+
$job = new FakeJob();
200+
$job->retryWithDelay(60);
201+
202+
$queue = new SyncQueue(new Container);
203+
$queue->setContainer(new Container);
204+
205+
$reflection = new \ReflectionClass($queue);
206+
$method = $reflection->getMethod('getJobBackoff');
207+
$method->setAccessible(true);
208+
209+
$backoff = $method->invoke($queue, $job);
210+
211+
$this->assertSame('60', $backoff);
212+
}
213+
214+
public function testRetryUntilPropertyIsUsedInQueuePayload(): void
215+
{
216+
$job = new FakeJob();
217+
$datetime = Carbon::now()->addDay();
218+
$job->retryUntil($datetime);
219+
220+
$queue = new SyncQueue(new Container);
221+
$queue->setContainer(new Container);
222+
223+
$reflection = new \ReflectionClass($queue);
224+
$method = $reflection->getMethod('getJobExpiration');
225+
$method->setAccessible(true);
226+
227+
$expiration = $method->invoke($queue, $job);
228+
229+
$this->assertSame($datetime->getTimestamp(), $expiration);
230+
}
231+
232+
public function testRetryUntilWithTimestampIsUsedInQueuePayload(): void
233+
{
234+
$job = new FakeJob();
235+
$timestamp = Carbon::now()->addDay()->getTimestamp();
236+
$job->retryUntil($timestamp);
237+
238+
$queue = new SyncQueue(new Container);
239+
$queue->setContainer(new Container);
240+
241+
$reflection = new \ReflectionClass($queue);
242+
$method = $reflection->getMethod('getJobExpiration');
243+
$method->setAccessible(true);
244+
245+
$expiration = $method->invoke($queue, $job);
246+
247+
$this->assertSame($timestamp, $expiration);
248+
}
249+
250+
public function testRetryWithDelayCanBeOverwritten(): void
251+
{
252+
$job = new FakeJob();
253+
$job->retryWithDelay(60);
254+
$job->retryWithDelay([10, 30]);
255+
256+
$this->assertSame($job->backoff, [10, 30]);
257+
}
258+
259+
public function testRetryUntilCanBeOverwritten(): void
260+
{
261+
$job = new FakeJob();
262+
$firstDatetime = Carbon::now()->addDay();
263+
$secondDatetime = Carbon::now()->addDays(2);
264+
265+
$job->retryUntil($firstDatetime);
266+
$job->retryUntil($secondDatetime);
267+
268+
$this->assertSame($job->retryUntil, $secondDatetime);
269+
}
68270
}
69271

70272
class FakeJob

0 commit comments

Comments
 (0)