Files
projectsend/app/Modules/Platform/Scheduling/Console/PurgeFailedJobsCommand.php
ignacionelson cab9291d29 Stop two tables from growing forever on an untended installation
Failed queue jobs and read notifications both grow with use, and neither
ever shrank on its own. The failed-jobs list waited for somebody to press
"Delete all failed" — a fine tool for a backlog you are looking at, and
the only thing that ever emptied it. Notifications had nothing at all: one
row per recipient per event, kept for the life of the installation, on
what is easily the fastest-growing table here.

Both now have a retention window, set together on the Scheduler screen
under Housekeeping, and a nightly purge that honours it. Thirty days for
failed jobs and ninety for read notifications, and zero means keep
everything — the explicit choice somebody makes when a failure is evidence
rather than debris.

Unread notifications are never deleted, whatever their age. A notification
nobody has looked at is the one row in that table still doing its job, and
somebody back from four months away should find their news rather than a
clean slate. The activity log is untouched by any of this: it is an audit
trail, and it is never pruned.

Two things came out of building it. The API request log purge has been
running nightly since it shipped without ever appearing on the Scheduler
screen — so a failure of it was invisible on the screen that exists to
make failures visible — and there is now a test asserting the screen's
list and the schedule are the same list, because they had already drifted
once and would again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:44:06 -03:00

64 lines
2.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Platform\Scheduling\Console;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Console\Command;
use Illuminate\Queue\Failed\FailedJobProviderInterface;
use Illuminate\Queue\Failed\PrunableFailedJobProvider;
/**
* Delete permanently failed queue jobs past the retention window.
*
* The Scheduler screen lists them and offers a "Delete all failed"
* button, which is the right tool for a backlog somebody is looking at —
* but it is the *only* thing that ever shrank this table. A mail server
* down overnight can leave thousands of rows, each carrying a serialized
* payload and an exception trace, on an installation whose administrator
* has no reason to open that screen for months.
*
* Deliberately its own `projectsend:` command rather than a scheduled
* `queue:prune-failed`, for two reasons: RecordsScheduledTaskRuns only
* tracks our own signatures, so anything else runs invisibly; and the
* retention window belongs in the settings with the rest of them rather
* than in a number written into routes/console.php.
*/
class PurgeFailedJobsCommand extends Command
{
protected $signature = 'projectsend:purge-failed-jobs';
protected $description = 'Delete failed queue jobs older than the configured retention window (runs daily)';
public function handle(Settings $settings, FailedJobProviderInterface $failer): int
{
$days = (int) $settings->get(Setting::FailedJobRetentionDays);
// 0 means keep indefinitely — an explicit choice for somebody who
// treats a failed job as evidence rather than as debris.
if ($days <= 0) {
$this->info('Failed job retention is disabled; nothing pruned.');
return self::SUCCESS;
}
// Both providers this application can be configured with implement
// it; the guard is here because the interface it is declared
// against does not require it, and a queue driver that cannot
// prune should say so rather than fail obscurely.
if (! $failer instanceof PrunableFailedJobProvider) {
$this->warn('This queue failure store cannot be pruned; nothing done.');
return self::SUCCESS;
}
$deleted = $failer->prune(now()->subDays($days));
$this->info("Pruned {$deleted} failed jobs older than {$days} days.");
return self::SUCCESS;
}
}