Skip to content

Commit

Permalink
[10.x] File failed job storage driver (laravel#47007)
Browse files Browse the repository at this point in the history
* first pass at file based failed jobs

* code cleaning

* use path verbatim

* use default value for path

* Adds tests for `FileFailedJobProvider` (laravel#47013)

* add limit option to file failed job provider

* fix order in test

* change default location

---------

Co-authored-by: Nuno Maduro <[email protected]>
  • Loading branch information
2 people authored and milwad-dev committed May 12, 2023
1 parent 6a40f60 commit 70ac24f
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 1 deletion.
168 changes: 168 additions & 0 deletions src/Illuminate/Queue/Failed/FileFailedJobProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace Illuminate\Queue\Failed;

use DateTimeInterface;
use Illuminate\Support\Facades\Date;

class FileFailedJobProvider implements FailedJobProviderInterface, PrunableFailedJobProvider
{
/**
* The file path where the failed job file should be stored.
*
* @var string
*/
protected $path;

/**
* The maximum number of failed jobs to retain.
*
* @var int
*/
protected $limit;

/**
* Create a new database failed job provider.
*
* @param string $path
* @param int $limit
* @return void
*/
public function __construct($path, $limit = 100)
{
$this->path = $path;
$this->limit = $limit;
}

/**
* Log a failed job into storage.
*
* @param string $connection
* @param string $queue
* @param string $payload
* @param \Throwable $exception
* @return int|null
*/
public function log($connection, $queue, $payload, $exception)
{
$id = json_decode($payload, true)['uuid'];

$jobs = $this->read();

$failedAt = Date::now();

array_unshift($jobs, [
'id' => $id,
'connection' => $connection,
'queue' => $queue,
'payload' => $payload,
'exception' => (string) mb_convert_encoding($exception, 'UTF-8'),
'failed_at' => $failedAt->format('Y-m-d H:i:s'),
'failed_at_timestamp' => $failedAt->getTimestamp(),
]);

$this->write(array_slice($jobs, 0, $this->limit));
}

/**
* Get a list of all of the failed jobs.
*
* @return array
*/
public function all()
{
return $this->read();
}

/**
* Get a single failed job.
*
* @param mixed $id
* @return object|null
*/
public function find($id)
{
return collect($this->read())
->first(fn ($job) => $job->id === $id);
}

/**
* Delete a single failed job from storage.
*
* @param mixed $id
* @return bool
*/
public function forget($id)
{
$this->write($pruned = collect($jobs = $this->read())
->reject(fn ($job) => $job->id === $id)
->values()
->all());

return count($jobs) !== count($pruned);
}

/**
* Flush all of the failed jobs from storage.
*
* @param int|null $hours
* @return void
*/
public function flush($hours = null)
{
$this->prune(Date::now()->subHours($hours ?: 0));
}

/**
* Prune all of the entries older than the given date.
*
* @param \DateTimeInterface $before
* @return int
*/
public function prune(DateTimeInterface $before)
{
$jobs = $this->read();

$this->write($prunedJobs = collect($jobs)->reject(function ($job) use ($before) {
return $job->failed_at_timestamp <= $before->getTimestamp();
})->values()->all());

return count($jobs) - count($prunedJobs);
}

/**
* Read the failed jobs file.
*
* @return array
*/
protected function read()
{
if (! file_exists($this->path)) {
return [];
}

$content = file_get_contents($this->path);

if (empty(trim($content))) {
return [];
}

$content = json_decode($content);

return is_array($content) ? $content : [];
}

/**
* Write the given array of jobs to the failed jobs file.
*
* @param array $jobs
* @return void
*/
protected function write(array $jobs)
{
file_put_contents(
$this->path,
json_encode($jobs, JSON_PRETTY_PRINT)
);
}
}
8 changes: 7 additions & 1 deletion src/Illuminate/Queue/QueueServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Queue\Failed\DatabaseUuidFailedJobProvider;
use Illuminate\Queue\Failed\DynamoDbFailedJobProvider;
use Illuminate\Queue\Failed\FileFailedJobProvider;
use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Facade;
Expand Down Expand Up @@ -250,7 +251,12 @@ protected function registerFailedJobServices()
return new NullFailedJobProvider;
}

if (isset($config['driver']) && $config['driver'] === 'dynamodb') {
if (isset($config['driver']) && $config['driver'] === 'file') {
return new FileFailedJobProvider(
$config['path'] ?? $this->app->storagePath('framework/cache/failed-jobs.json'),
$config['limit'] ?? 100,
);
} elseif (isset($config['driver']) && $config['driver'] === 'dynamodb') {
return $this->dynamoFailedJobProvider($config);
} elseif (isset($config['driver']) && $config['driver'] === 'database-uuids') {
return $this->databaseUuidFailedJobProvider($config);
Expand Down
153 changes: 153 additions & 0 deletions tests/Queue/FileFailedJobProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

namespace Illuminate\Tests\Queue;

use Exception;
use Illuminate\Queue\Failed\FileFailedJobProvider;
use Illuminate\Support\Str;
use PHPUnit\Framework\TestCase;

class FileFailedJobProviderTest extends TestCase
{
protected $path;

protected $provider;

protected function setUp(): void
{
$this->path = @tempnam('tmp', 'file_failed_job_provider_test');
$this->provider = new FileFailedJobProvider($this->path);
}

public function testCanLogFailedJobs()
{
[$uuid, $exception] = $this->logFailedJob();

$failedJobs = $this->provider->all();

$this->assertEquals([
(object) [
'id' => $uuid,
'connection' => 'connection',
'queue' => 'queue',
'payload' => json_encode(['uuid' => $uuid]),
'exception' => (string) mb_convert_encoding($exception, 'UTF-8'),
'failed_at' => $failedJobs[0]->failed_at,
'failed_at_timestamp' => $failedJobs[0]->failed_at_timestamp,
],
], $failedJobs);
}

public function testCanRetrieveAllFailedJobs()
{
[$uuidOne, $exceptionOne] = $this->logFailedJob();
[$uuidTwo, $exceptionTwo] = $this->logFailedJob();

$failedJobs = $this->provider->all();

$this->assertEquals([
(object) [
'id' => $uuidTwo,
'connection' => 'connection',
'queue' => 'queue',
'payload' => json_encode(['uuid' => $uuidTwo]),
'exception' => (string) mb_convert_encoding($exceptionTwo, 'UTF-8'),
'failed_at' => $failedJobs[1]->failed_at,
'failed_at_timestamp' => $failedJobs[1]->failed_at_timestamp,
],
(object) [
'id' => $uuidOne,
'connection' => 'connection',
'queue' => 'queue',
'payload' => json_encode(['uuid' => $uuidOne]),
'exception' => (string) mb_convert_encoding($exceptionOne, 'UTF-8'),
'failed_at' => $failedJobs[0]->failed_at,
'failed_at_timestamp' => $failedJobs[0]->failed_at_timestamp,
],
], $failedJobs);
}

public function testCanFindFailedJobs()
{
[$uuid, $exception] = $this->logFailedJob();

$failedJob = $this->provider->find($uuid);

$this->assertEquals((object) [
'id' => $uuid,
'connection' => 'connection',
'queue' => 'queue',
'payload' => json_encode(['uuid' => (string) $uuid]),
'exception' => (string) mb_convert_encoding($exception, 'UTF-8'),
'failed_at' => $failedJob->failed_at,
'failed_at_timestamp' => $failedJob->failed_at_timestamp,
], $failedJob);
}

public function testNullIsReturnedIfJobNotFound()
{
$uuid = Str::uuid();

$failedJob = $this->provider->find($uuid);

$this->assertNull($failedJob);
}

public function testCanForgetFailedJobs()
{
[$uuid] = $this->logFailedJob();

$this->provider->forget($uuid);

$failedJob = $this->provider->find($uuid);

$this->assertNull($failedJob);
}

public function testCanFlushFailedJobs()
{
$this->logFailedJob();
$this->logFailedJob();

$this->provider->flush();

$failedJobs = $this->provider->all();

$this->assertEmpty($failedJobs);
}

public function testCanPruneFailedJobs()
{
$this->logFailedJob();
$this->logFailedJob();

$this->provider->prune(now()->addDay(1));
$failedJobs = $this->provider->all();
$this->assertEmpty($failedJobs);

$this->logFailedJob();
$this->logFailedJob();

$this->provider->prune(now()->subDay(1));
$failedJobs = $this->provider->all();
$this->assertCount(2, $failedJobs);
}

public function testEmptyFailedJobsByDefault()
{
$failedJobs = $this->provider->all();

$this->assertEmpty($failedJobs);
}

public function logFailedJob()
{
$uuid = Str::uuid();

$exception = new Exception("Something went wrong at job [{$uuid}].");

$this->provider->log('connection', 'queue', json_encode(['uuid' => (string) $uuid]), $exception);

return [(string) $uuid, $exception];
}
}

0 comments on commit 70ac24f

Please sign in to comment.