Skip to content

Commit 0544f56

Browse files
Merge pull request #14 from stackkit/feature/failed-jobs-table
Feature/failed jobs table
2 parents 7b050e2 + 9cfc3ae commit 0544f56

9 files changed

+185
-29
lines changed

CHANGELOG.md

+6
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+
## 2.1.0-beta1 - 2021-03-28
8+
9+
**Added**
10+
11+
- Handling of failed jobs
12+
713
## 2.0.1 - 2020-12-06
814

915
**Fixed**

phpunit.xml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<env name="MAIL_DRIVER" value="log"/>
2424
<env name="QUEUE_DRIVER" value="cloudtasks"/>
2525
<env name="GOOGLE_APPLICATION_CREDENTIALS" value="./tests/Support/gcloud-key-valid.json" />
26+
<env name="DB_CONNECTION" value="sqlite" />
2627
</php>
2728

2829
<filter>

src/CloudTasksJob.php

+26-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ class CloudTasksJob extends LaravelJob implements JobContract
1010
{
1111
private $job;
1212
private $attempts;
13+
private $maxTries;
1314

14-
public function __construct($job, $attempts)
15+
public function __construct($job)
1516
{
1617
$this->job = $job;
17-
$this->attempts = $attempts;
1818
$this->container = Container::getInstance();
1919
}
2020

@@ -32,4 +32,28 @@ public function attempts()
3232
{
3333
return $this->attempts;
3434
}
35+
36+
public function setAttempts($attempts)
37+
{
38+
$this->attempts = $attempts;
39+
}
40+
41+
public function setMaxTries($maxTries)
42+
{
43+
if ((int) $maxTries === -1) {
44+
$maxTries = null;
45+
}
46+
47+
$this->maxTries = $maxTries;
48+
}
49+
50+
public function maxTries()
51+
{
52+
return $this->maxTries;
53+
}
54+
55+
public function setQueue($queue)
56+
{
57+
$this->queue = $queue;
58+
}
3559
}

src/TaskHandler.php

+38-7
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
44

55
use Google\Cloud\Tasks\V2\CloudTasksClient;
66
use Illuminate\Http\Request;
7+
use Illuminate\Queue\Events\JobFailed;
78
use Illuminate\Queue\Worker;
89
use Illuminate\Queue\WorkerOptions;
9-
use Firebase\JWT\JWT;
1010

1111
class TaskHandler
1212
{
1313
private $request;
14-
private $guzzle;
15-
private $jwt;
1614
private $publicKey;
1715

18-
public function __construct(CloudTasksClient $client, Request $request, JWT $jwt, OpenIdVerificator $publicKey)
16+
public function __construct(CloudTasksClient $client, Request $request, OpenIdVerificator $publicKey)
1917
{
2018
$this->client = $client;
2119
$this->request = $request;
22-
$this->jwt = $jwt;
2320
$this->publicKey = $publicKey;
2421
}
2522

@@ -33,6 +30,8 @@ public function handle($task = null)
3330

3431
$task = $task ?: $this->captureTask();
3532

33+
$this->listenForEvents();
34+
3635
$this->handleTask($task);
3736
}
3837

@@ -81,7 +80,11 @@ private function captureTask()
8180
{
8281
$input = file_get_contents('php://input');
8382

84-
if ($input === false) {
83+
if (!$input) {
84+
$input = request('input') ?: false;
85+
}
86+
87+
if (!$input) {
8588
throw new CloudTasksException('Could not read incoming task');
8689
}
8790

@@ -94,19 +97,47 @@ private function captureTask()
9497
return $task;
9598
}
9699

100+
private function listenForEvents()
101+
{
102+
app('events')->listen(JobFailed::class, function ($event) {
103+
app('queue.failer')->log(
104+
'cloudtasks', $event->job->getQueue(),
105+
$event->job->getRawBody(), $event->exception
106+
);
107+
});
108+
}
109+
97110
/**
98111
* @param $task
99112
* @throws CloudTasksException
100113
*/
101114
private function handleTask($task)
102115
{
103-
$job = new CloudTasksJob($task, request()->header('X-CloudTasks-TaskRetryCount'));
116+
$job = new CloudTasksJob($task);
117+
118+
$job->setAttempts(request()->header('X-CloudTasks-TaskRetryCount') + 1);
119+
$job->setQueue(request()->header('X-Cloudtasks-Queuename'));
120+
$job->setMaxTries($this->getQueueMaxTries($job));
104121

105122
$worker = $this->getQueueWorker();
106123

107124
$worker->process('cloudtasks', $job, new WorkerOptions());
108125
}
109126

127+
private function getQueueMaxTries(CloudTasksJob $job)
128+
{
129+
$queueName = $this->client->queueName(
130+
Config::project(),
131+
Config::location(),
132+
$job->getQueue()
133+
);
134+
135+
return $this->client
136+
->getQueue($queueName)
137+
->getRetryConfig()
138+
->getMaxAttempts();
139+
}
140+
110141
/**
111142
* @return Worker
112143
*/

tests/GooglePublicKeyTest.php

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Illuminate\Cache\Events\CacheHit;
99
use Illuminate\Cache\Events\CacheMissed;
1010
use Illuminate\Cache\Events\KeyWritten;
11-
use Illuminate\Support\Facades\Cache;
1211
use Illuminate\Support\Facades\Event;
1312
use Mockery;
1413
use phpseclib\Crypt\RSA;

tests/Support/FailingJob.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Tests\Support;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Bus\Dispatchable;
8+
use Illuminate\Queue\InteractsWithQueue;
9+
use Illuminate\Queue\SerializesModels;
10+
11+
class FailingJob implements ShouldQueue
12+
{
13+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
14+
15+
/**
16+
* Create a new job instance.
17+
*
18+
* @return void
19+
*/
20+
public function __construct()
21+
{
22+
//
23+
}
24+
25+
/**
26+
* Execute the job.
27+
*
28+
* @return void
29+
*/
30+
public function handle()
31+
{
32+
throw new \Error('simulating a failing job');
33+
}
34+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"uuid":"f4e1ea03-3ab9-45f8-a4a1-50218169472e","displayName":"Tests\\Support\\FailingJob","job":"Illuminate\\Queue\\CallQueuedHandler@call","maxTries":null,"maxExceptions":null,"delay":null,"timeout":null,"timeoutAt":null,"data":{"commandName":"Tests\\Support\\FailingJob","command":"O:24:\"Tests\\Support\\FailingJob\":8:{s:3:\"job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:5:\"delay\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:0:{}}"}}

tests/TaskHandlerTest.php

+54-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Google\Cloud\Tasks\V2\CloudTasksClient;
88
use Illuminate\Cache\Events\CacheHit;
99
use Illuminate\Cache\Events\KeyWritten;
10+
use Illuminate\Support\Facades\DB;
1011
use Illuminate\Support\Facades\Event;
1112
use Illuminate\Support\Facades\Mail;
1213
use Mockery;
@@ -45,10 +46,23 @@ protected function setUp(): void
4546
$googlePublicKey->shouldReceive('getPublicKey')->andReturnNull();
4647
$googlePublicKey->shouldReceive('getKidFromOpenIdToken')->andReturnNull();
4748

49+
$cloudTasksClient = Mockery::mock(new CloudTasksClient());
50+
51+
// Ensure we don't fetch the Queue name and attempts each test...
52+
$cloudTasksClient->shouldReceive('queueName')->andReturn('my-queue');
53+
$cloudTasksClient->shouldReceive('getQueue')->andReturn(new class {
54+
public function getRetryConfig() {
55+
return new class {
56+
public function getMaxAttempts() {
57+
return 3;
58+
}
59+
};
60+
}
61+
});
62+
4863
$this->handler = new TaskHandler(
49-
new CloudTasksClient(),
64+
$cloudTasksClient,
5065
request(),
51-
$this->jwt,
5266
$googlePublicKey
5367
);
5468

@@ -66,21 +80,6 @@ public function it_needs_an_authorization_header()
6680
$this->handler->handle();
6781
}
6882

69-
/** @test */
70-
public function the_authorization_header_must_contain_a_valid_gcloud_token()
71-
{
72-
request()->headers->add([
73-
'Authorization' => 'Bearer 123',
74-
]);
75-
76-
$this->expectException(CloudTasksException::class);
77-
$this->expectExceptionMessage('Could not decode incoming task');
78-
79-
$this->handler->handle();
80-
81-
// @todo - test with a valid token, not sure how to do that right now
82-
}
83-
8483
/** @test */
8584
public function it_will_validate_the_token_iss()
8685
{
@@ -144,8 +143,46 @@ public function it_runs_the_incoming_job()
144143
Mail::assertSent(TestMailable::class);
145144
}
146145

146+
/** @test */
147+
public function after_max_attempts_it_will_log_to_failed_table()
148+
{
149+
$this->request->headers->add(['X-Cloudtasks-Queuename' => 'my-queue']);
150+
151+
$this->request->headers->add(['X-CloudTasks-TaskRetryCount' => 1]);
152+
try {
153+
$this->handler->handle($this->failingJob());
154+
} catch (\Throwable $e) {
155+
//
156+
}
157+
158+
$this->assertCount(0, DB::table('failed_jobs')->get());
159+
160+
$this->request->headers->add(['X-CloudTasks-TaskRetryCount' => 2]);
161+
try {
162+
$this->handler->handle($this->failingJob());
163+
} catch (\Throwable $e) {
164+
//
165+
}
166+
167+
$this->assertDatabaseHas('failed_jobs', [
168+
'connection' => 'cloudtasks',
169+
'queue' => 'my-queue',
170+
'payload' => rtrim($this->failingJobPayload()),
171+
]);
172+
}
173+
147174
private function simpleJob()
148175
{
149176
return json_decode(file_get_contents(__DIR__ . '/Support/test-job-payload.json'), true);
150177
}
178+
179+
private function failingJobPayload()
180+
{
181+
return file_get_contents(__DIR__ . '/Support/failing-job-payload.json');
182+
}
183+
184+
private function failingJob()
185+
{
186+
return json_decode($this->failingJobPayload(), true);
187+
}
151188
}

tests/TestCase.php

+25-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,33 @@
22

33
namespace Tests;
44

5-
use Illuminate\Support\Facades\Artisan;
6-
75
class TestCase extends \Orchestra\Testbench\TestCase
86
{
7+
public static $migrated = false;
8+
9+
protected function setUp(): void
10+
{
11+
parent::setUp();
12+
13+
// There is probably a more sane way to do this
14+
if (!static::$migrated) {
15+
if (file_exists(database_path('database.sqlite'))) {
16+
unlink(database_path('database.sqlite'));
17+
}
18+
19+
touch(database_path('database.sqlite'));
20+
21+
foreach(glob(database_path('migrations/*.php')) as $file) {
22+
unlink($file);
23+
}
24+
25+
$this->artisan('queue:failed-table');
26+
$this->artisan('migrate');
27+
28+
static::$migrated = true;
29+
}
30+
}
31+
932
/**
1033
* Get package providers. At a minimum this is the package being tested, but also
1134
* would include packages upon which our package depends, e.g. Cartalyst/Sentry

0 commit comments

Comments
 (0)