Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/resource-cleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
| implementing the CleanableResource contract.
|
*/
'default_retention_days' => env('RESOURCE_CLEANUP_RETENTION_DAYS', 90),
'default_retention_days' => env('RESOURCE_CLEANUP_RETENTION_DAYS', 180),

/*
|--------------------------------------------------------------------------
Expand Down
3 changes: 1 addition & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
stopOnFailure="false"
>
<testsuites>
<testsuite name="Unit">
Expand Down
25 changes: 20 additions & 5 deletions src/Console/Commands/ResourceCleanupCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class ResourceCleanupCommand extends Command
protected $signature = 'resource-cleanup:run
{--model=* : One or more fully-qualified model class names to clean up}
{--dry-run : Show what would be deleted without actually deleting}
{--skip-index-check : Skip the created_at index validation (not recommended, may cause slow queries)}';
{--skip-index-check : Skip the created_at index validation (not recommended, may cause slow queries)}
{--limit= : Maximum number of records to delete per model (0 or omitted means no limit)}';

protected $description = 'Permanently delete records older than the configured cutoff date.';

Expand All @@ -44,6 +45,8 @@ public function handle(): int
return self::FAILURE;
}

$limit = (int) ($this->option('limit') ?? 0);

$totalDeleted = 0;
foreach ($models as $modelClass) {
try {
Expand All @@ -56,22 +59,34 @@ public function handle(): int

if ($this->option('dry-run')) {
$count = $query->count();
$effective = $limit > 0 ? min($count, $limit) : $count;

$this->line(sprintf('[dry-run] %s: %d record(s) would be deleted.', $modelClass, $count));
$this->line(sprintf('[dry-run] %s: %d record(s) would be deleted.', $modelClass, $effective));

$totalDeleted += $count;
$totalDeleted += $effective;

continue;
}

$deleted = 0;
$query->chunkById(
config('resource-cleanup.cleanup_chunk_size'),
function (Collection $records) use ($modelClass, &$deleted) {
$deleted += $modelClass::query()->whereIn('id', $records->pluck('id'))->forceDelete();
function (Collection $records) use ($modelClass, &$deleted, $limit) {
$ids = $records->pluck('id');

if ($limit > 0) {
$ids = $ids->take($limit - $deleted);
}

$deleted += $modelClass::query()->whereIn('id', $ids)->forceDelete();

// 25ms sleep gives the DB breathing room for consecutive queries
usleep(25000);

// break out of query chunk loop if limit is reached
if ($limit > 0 && $deleted >= $limit) {
return false;
}
},
);

Expand Down
54 changes: 48 additions & 6 deletions tests/Feature/ResourceCleanupCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function test_dry_run_without_soft_deletes(): void
{
$this->app['config']->set('resource-cleanup.models', [TestResource::class]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestResource::create(['name' => 'old-1', 'created_at' => $old]);
TestResource::create(['name' => 'old-2', 'created_at' => $old]);
TestResource::create(['name' => 'old-3', 'created_at' => $old]);
Expand All @@ -40,7 +40,7 @@ public function test_dry_run_with_soft_deletes(): void
{
$this->app['config']->set('resource-cleanup.models', [TestSoftDeletableResource::class]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestSoftDeletableResource::create(['name' => 'old-1', 'created_at' => $old])->delete();
TestSoftDeletableResource::create(['name' => 'old-2', 'created_at' => $old])->delete();
TestSoftDeletableResource::create(['name' => 'old-3', 'created_at' => $old])->delete();
Expand All @@ -61,7 +61,7 @@ public function test_resource_cleanup_deletes_old_records_without_soft_deletes()
{
$this->app['config']->set('resource-cleanup.models', [TestResource::class]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestResource::create(['name' => 'old-1', 'created_at' => $old]);
TestResource::create(['name' => 'old-2', 'created_at' => $old]);
TestResource::create(['name' => 'old-3', 'created_at' => $old]);
Expand All @@ -80,7 +80,7 @@ public function test_resource_cleanup_deletes_old_soft_deleted_records(): void
{
$this->app['config']->set('resource-cleanup.models', [TestSoftDeletableResource::class]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestSoftDeletableResource::create(['name' => 'old-deleted-1', 'created_at' => $old])->delete();
TestSoftDeletableResource::create(['name' => 'old-deleted-2', 'created_at' => $old])->delete();
TestSoftDeletableResource::create(['name' => 'old-not-deleted', 'created_at' => $old]);
Expand Down Expand Up @@ -118,7 +118,7 @@ public function test_resource_cleanup_valid_model_options(): void
TestCleanableSoftDeletableResource::class,
]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestSoftDeletableResource::create(['name' => 'old', 'created_at' => $old])->delete();

$this->artisan('resource-cleanup:run', ['--model' => [TestSoftDeletableResource::class]])
Expand Down Expand Up @@ -196,6 +196,48 @@ public function test_resource_cleanup_uses_scope_cleanable_on_soft_deletable_cle
$this->assertSame(0, TestCleanableSoftDeletableResource::withTrashed()->where('name', 'older-than-one-year')->count());
}

// -------------------------------------------------------------------------
// Limit option
// -------------------------------------------------------------------------

public function test_resource_cleanup_deletes_at_most_limit_records(): void
{
$this->app['config']->set('resource-cleanup.models', [TestResource::class]);

$old = Carbon::now()->subDays(181);
TestResource::create(['name' => 'old-1', 'created_at' => $old]);
TestResource::create(['name' => 'old-2', 'created_at' => $old]);
TestResource::create(['name' => 'old-3', 'created_at' => $old]);
TestResource::create(['name' => 'old-4', 'created_at' => $old]);
TestResource::create(['name' => 'old-5', 'created_at' => $old]);

$this->artisan('resource-cleanup:run', ['--limit' => 3])
->expectsOutput(TestResource::class . ': 3 record(s) deleted.')
->expectsOutput('Done. Total: 3 record(s) deleted.')
->assertSuccessful();

$this->assertSame(2, TestResource::count());
}

public function test_dry_run_respects_limit_option(): void
{
$this->app['config']->set('resource-cleanup.models', [TestResource::class]);

$old = Carbon::now()->subDays(181);
TestResource::create(['name' => 'old-1', 'created_at' => $old]);
TestResource::create(['name' => 'old-2', 'created_at' => $old]);
TestResource::create(['name' => 'old-3', 'created_at' => $old]);
TestResource::create(['name' => 'old-4', 'created_at' => $old]);
TestResource::create(['name' => 'old-5', 'created_at' => $old]);

$this->artisan('resource-cleanup:run', ['--dry-run' => true, '--limit' => 3])
->expectsOutput('[dry-run] ' . TestResource::class . ': 3 record(s) would be deleted.')
->expectsOutput('Done. Total: 3 record(s) would be deleted.')
->assertSuccessful();

$this->assertSame(5, TestResource::count());
}

// -------------------------------------------------------------------------
// Failure cases
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -231,7 +273,7 @@ public function test_resource_cleanup_skips_index_check_when_flag_is_provided():
{
$this->app['config']->set('resource-cleanup.models', [TestResourceWithoutIndex::class]);

$old = Carbon::now()->subDays(91);
$old = Carbon::now()->subDays(181);
TestResourceWithoutIndex::create(['name' => 'old', 'created_at' => $old]);
TestResourceWithoutIndex::create(['name' => 'recent']);

Expand Down
Loading