Skip to content

Commit 069c30e

Browse files
Merge pull request #70 from stackkit/feature/job-releasing
Job releasing
2 parents d54ba2e + 90b68f6 commit 069c30e

32 files changed

+896
-88
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 3.?.0 - ????-??-??
8+
9+
**Added**
10+
11+
- Jobs can now be released back onto the queue.
12+
713
## 3.2.1 - 2022-09-02
814

915
**Fixed**

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Please check the table below for supported Laravel and PHP versions:
5656
// does not respond by this deadline then the request is cancelled and the attempt
5757
// is marked as a DEADLINE_EXCEEDED failure.
5858
'dispatch_deadline' => null,
59+
'backoff' => 0,
5960
],
6061
```
6162

@@ -79,13 +80,19 @@ Please check the table below on what the values mean and what their value should
7980
</details>
8081
<details>
8182
<summary>
82-
How it works
83+
How it works & Differences
8384
</summary>
8485
<br>
8586
Using Cloud Tasks as a Laravel queue driver is fundamentally different than other Laravel queue drivers, like Redis.
8687

8788
Typically a Laravel queue has a worker that listens to incoming jobs using the `queue:work` / `queue:listen` command.
8889
With Cloud Tasks, this is not the case. Instead, Cloud Tasks will schedule the job for you and make an HTTP request to your application with the job payload. There is no need to run a `queue:work/listen` command.
90+
91+
#### Good to know
92+
93+
- The "Min backoff" and "Max backoff" options in Cloud Tasks are ignored. This is intentional: Laravel has its own backoff feature (which is more powerful than what Cloud Tasks offers) and therefore I have chosen that over the Cloud Tasks one.
94+
- Similarly to the backoff feature, I have also chosen to let the package do job retries the 'Laravel way'. In Cloud Tasks, when a task throws an exception, Cloud Tasks will decide for itself when to retry the task (based on the backoff values). It will also manage its own state and knows how many times a task has been retried. This is different from Laravel. In typical Laravel queues, when a job throws an exception, the job is deleted and released back onto the queue. In order to support Laravel's backoff feature, this package must behave the same way about job retries.
95+
8996
</details>
9097
<details>
9198
<summary>Dashboard (beta)</summary>

composer.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,23 @@
3636
"Stackkit\\LaravelGoogleCloudTasksQueue\\CloudTasksServiceProvider"
3737
]
3838
}
39+
},
40+
"scripts": {
41+
"l9": [
42+
"composer require laravel/framework:9.* orchestra/testbench:7.* --no-interaction --no-update",
43+
"composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
44+
],
45+
"l8": [
46+
"composer require laravel/framework:8.* orchestra/testbench:6.* --no-interaction --no-update",
47+
"composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
48+
],
49+
"l7": [
50+
"composer require laravel/framework:7.* orchestra/testbench:5.* --no-interaction --no-update",
51+
"composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
52+
],
53+
"l6": [
54+
"composer require laravel/framework:6.* orchestra/testbench:4.* --no-interaction --no-update",
55+
"composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
56+
]
3957
}
4058
}

dashboard/dist/assets/index.1002db9a.css renamed to dashboard/dist/assets/index.d8eef428.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dashboard/dist/assets/index.5a46c6a0.js renamed to dashboard/dist/assets/index.ea68d73f.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dashboard/dist/crossword.png

-42.7 KB
Binary file not shown.

dashboard/dist/dot-grid.png

-25.4 KB
Binary file not shown.

dashboard/dist/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
<link rel="icon" href="/favicon.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Vite App</title>
8-
<script type="module" crossorigin src="/assets/index.5a46c6a0.js"></script>
8+
<script type="module" crossorigin src="/assets/index.ea68d73f.js"></script>
99
<link rel="modulepreload" href="/assets/vendor.433de25e.js">
10-
<link rel="stylesheet" href="/assets/index.1002db9a.css">
10+
<link rel="stylesheet" href="/assets/index.d8eef428.css">
1111
</head>
1212
<body class="bg-gray-100">
1313
<div id="app"></div>

dashboard/dist/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"index.html": {
3-
"file": "assets/index.5a46c6a0.js",
3+
"file": "assets/index.ea68d73f.js",
44
"src": "index.html",
55
"isEntry": true,
66
"imports": [
77
"_vendor.433de25e.js"
88
],
99
"css": [
10-
"assets/index.1002db9a.css"
10+
"assets/index.d8eef428.css"
1111
]
1212
},
1313
"_vendor.433de25e.js": {

dashboard/dist/pw_maze_white.png

-600 Bytes
Binary file not shown.

dashboard/src/components/Status.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function ucfirst(input) {
3131
.task-queued, .task-scheduled {
3232
@apply bg-gray-100 text-gray-500
3333
}
34-
.task-running {
34+
.task-running, .task-released {
3535
@apply bg-blue-100 text-blue-800
3636
}
3737
</style>

dashboard/src/components/Task.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const titles = {
2727
successful: 'Successful',
2828
error: 'An error occurred',
2929
failed: 'Failed permanently',
30+
released: 'Released',
3031
}
3132
</script>
3233

@@ -60,6 +61,13 @@ const titles = {
6061
Scheduled: {{ event['scheduled_at'] }} (UTC)
6162
</span>
6263
</div>
64+
<div v-if="event['delay']">
65+
<span
66+
class="bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"
67+
>
68+
Delay: {{ event['delay'] }} seconds
69+
</span>
70+
</div>
6371
</h3>
6472
<Popper
6573
:content="event.datetime"

phpunit.xml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
stopOnFailure="false">
1111
<testsuites>
1212
<testsuite name="Testsuite">
13-
<file>./tests/ConfigTest.php</file>
14-
<file>./tests/TaskHandlerTest.php</file>
15-
<file>./tests/CloudTasksApiTest.php</file>
16-
<file>./tests/CloudTasksDashboardTest.php</file>
13+
<directory suffix="Test.php">./tests</directory>
1714
</testsuite>
1815
</testsuites>
1916
<php>

src/CloudTasksApiFake.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,9 @@ public function assertTaskCreated(Closure $closure): void
9191

9292
Assert::assertTrue($count > 0, 'Task was not created.');
9393
}
94+
95+
public function assertCreatedTaskCount(int $count): void
96+
{
97+
Assert::assertCount($count, $this->createdTasks);
98+
}
9499
}

src/CloudTasksJob.php

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
namespace Stackkit\LaravelGoogleCloudTasksQueue;
44

55
use Illuminate\Container\Container;
6-
use Illuminate\Queue\Jobs\Job as LaravelJob;
76
use Illuminate\Contracts\Queue\Job as JobContract;
7+
use Illuminate\Queue\Jobs\Job as LaravelJob;
8+
use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
9+
use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException;
810
use function Safe\json_encode;
911

1012
class CloudTasksJob extends LaravelJob implements JobContract
1113
{
12-
private array $job;
13-
private ?int $attempts;
14+
/**
15+
* The Cloud Tasks raw job payload (request payload).
16+
*
17+
* @var array
18+
*/
19+
public array $job;
20+
1421
private ?int $maxTries;
1522
public ?int $retryUntil = null;
1623

@@ -29,6 +36,11 @@ public function __construct(array $job, CloudTasksQueue $cloudTasksQueue)
2936
$this->queue = $command['queue'] ?? config('queue.connections.' .config('queue.default') . '.queue');
3037
}
3138

39+
public function job()
40+
{
41+
return $this->job;
42+
}
43+
3244
public function getJobId(): string
3345
{
3446
return $this->job['uuid'];
@@ -46,12 +58,12 @@ public function getRawBody(): string
4658

4759
public function attempts(): ?int
4860
{
49-
return $this->attempts;
61+
return $this->job['internal']['attempts'];
5062
}
5163

5264
public function setAttempts(int $attempts): void
5365
{
54-
$this->attempts = $attempts;
66+
$this->job['internal']['attempts'] = $attempts;
5567
}
5668

5769
public function setMaxTries(int $maxTries): void
@@ -95,4 +107,28 @@ public function delete(): void
95107

96108
$this->cloudTasksQueue->delete($this);
97109
}
110+
111+
public function release($delay = 0)
112+
{
113+
parent::release();
114+
115+
$this->cloudTasksQueue->release($this, $delay);
116+
117+
$properties = TaskHandler::getCommandProperties($this->job['data']['command']);
118+
$connection = $properties['connection'] ?? config('queue.default');
119+
120+
// The package uses the JobReleasedAfterException provided by Laravel to grab
121+
// the payload of the released job in tests to easily run and test a released
122+
// job. Because the event is only accessible in Laravel 9.x, we create an
123+
// identical event to hook into for Laravel versions older than 9.x
124+
if (version_compare(app()->version(), '9.0.0', '<')) {
125+
if (data_get($this->job, 'internal.errored')) {
126+
app('events')->dispatch(new JobReleasedAfterException($connection, $this));
127+
}
128+
}
129+
130+
if (! data_get($this->job, 'internal.errored')) {
131+
app('events')->dispatch(new JobReleased($connection, $this, $delay));
132+
}
133+
}
98134
}

src/CloudTasksQueue.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
use Illuminate\Contracts\Queue\Queue as QueueContract;
1313
use Illuminate\Queue\Queue as LaravelQueue;
1414
use Illuminate\Support\Str;
15-
use function Safe\json_encode;
15+
use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated;
1616
use function Safe\json_decode;
17+
use function Safe\json_encode;
1718

1819
class CloudTasksQueue extends LaravelQueue implements QueueContract
1920
{
@@ -93,7 +94,9 @@ function ($payload, $queue) {
9394
*/
9495
public function pushRaw($payload, $queue = null, array $options = [])
9596
{
96-
return $this->pushToCloudTasks($queue, $payload);
97+
$delay = ! empty($options['delay']) ? $options['delay'] : 0;
98+
99+
$this->pushToCloudTasks($queue, $payload, $delay);
97100
}
98101

99102
/**
@@ -141,11 +144,19 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0)
141144
// we will add it manually here if it's not present yet.
142145
[$payload, $uuid] = $this->withUuid($payload);
143146

147+
// Since 3.x tasks are released back onto the queue after an exception has
148+
// been thrown. This means we lose the native [X-CloudTasks-TaskRetryCount] header
149+
// value and need to manually set and update the number of times a task has been attempted.
150+
$payload = $this->withAttempts($payload);
151+
144152
$httpRequest->setBody($payload);
145153

146154
$task = $this->createTask();
147155
$task->setHttpRequest($httpRequest);
148156

157+
// The deadline for requests sent to the app. If the app does not respond by
158+
// this deadline then the request is cancelled and the attempt is marked as
159+
// a failure. Cloud Tasks will retry the task according to the RetryConfig.
149160
if (!empty($this->config['dispatch_deadline'])) {
150161
$task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']]));
151162
}
@@ -180,6 +191,18 @@ private function withUuid(string $payload): array
180191
];
181192
}
182193

194+
private function withAttempts(string $payload): string
195+
{
196+
/** @var array $decoded */
197+
$decoded = json_decode($payload, true);
198+
199+
if (!isset($decoded['internal']['attempts'])) {
200+
$decoded['internal']['attempts'] = 0;
201+
}
202+
203+
return json_encode($decoded);
204+
}
205+
183206
/**
184207
* Pop the next job off of the queue.
185208
*
@@ -217,6 +240,17 @@ public function delete(CloudTasksJob $job): void
217240
CloudTasksApi::deleteTask($taskName);
218241
}
219242

243+
public function release(CloudTasksJob $job, int $delay = 0): void
244+
{
245+
$job->delete();
246+
247+
$payload = $job->getRawBody();
248+
249+
$options = ['delay' => $delay];
250+
251+
$this->pushRaw($payload, $job->getQueue(), $options);
252+
}
253+
220254
private function createTask(): Task
221255
{
222256
return app(Task::class);

src/CloudTasksServiceProvider.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Illuminate\Queue\Events\JobProcessed;
99
use Illuminate\Queue\Events\JobProcessing;
1010
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
11+
use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
12+
use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated;
1113
use function Safe\file_get_contents;
1214
use function Safe\json_decode;
1315

@@ -158,6 +160,8 @@ private function registerDashboard(): void
158160
});
159161

160162
app('events')->listen(JobProcessed::class, function (JobProcessed $event) {
163+
data_set($event->job->job, 'internal.processed', true);
164+
161165
if (!CloudTasks::dashboardEnabled()) {
162166
return;
163167
}
@@ -168,6 +172,8 @@ private function registerDashboard(): void
168172
});
169173

170174
app('events')->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) {
175+
data_set($event->job->job, 'internal.errored', true);
176+
171177
if (!CloudTasks::dashboardEnabled()) {
172178
return;
173179
}
@@ -182,5 +188,13 @@ private function registerDashboard(): void
182188

183189
DashboardService::make()->markAsFailed($event);
184190
});
191+
192+
app('events')->listen(JobReleased::class, function (JobReleased $event) {
193+
if (!CloudTasks::dashboardEnabled()) {
194+
return;
195+
}
196+
197+
DashboardService::make()->markAsReleased($event);
198+
});
185199
}
186200
}

0 commit comments

Comments
 (0)