forked from laravel/framework
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[10.x] File failed job storage driver (laravel#47007)
* 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
1 parent
6a40f60
commit 70ac24f
Showing
3 changed files
with
328 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |