mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(docker-cleanup): skip scheduled cleanups while a deployment is running
Closes #12183 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e853f3d136
commit
2a7faa1210
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Support\Actions\UniqueUntilProcessingJobDecorator;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
@@ -83,9 +85,24 @@ class CleanupDocker implements ShouldBeUnique
|
||||
return 'docker-cleanup-last-run-'.$server->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* The deployment that is queued or running on the server (as target or build server), if any.
|
||||
* The cleanup prunes the build caches, which fails a build that uses them. A deployment that
|
||||
* wrote no log for longer than the deployment timeout is stuck and does not count.
|
||||
*/
|
||||
public static function activeDeployment(Server $server): ?ApplicationDeploymentQueue
|
||||
{
|
||||
return ApplicationDeploymentQueue::query()
|
||||
->where(fn ($query) => $query->where('server_id', $server->id)->orWhere('build_server_id', $server->id))
|
||||
->whereIn('status', [ApplicationDeploymentStatus::QUEUED->value, ApplicationDeploymentStatus::IN_PROGRESS->value])
|
||||
->where('updated_at', '>=', now()->subSeconds($server->settings->dynamic_timeout ?? 3600))
|
||||
->first(['id', 'deployment_uuid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup after a resource stop: skipped when a cleanup ran recently, and the queued run
|
||||
* only cleans when the disk usage is at or above the server cleanup threshold.
|
||||
* only cleans when the disk usage is at or above the server cleanup threshold and no
|
||||
* deployment is active on the server.
|
||||
*/
|
||||
public static function dispatchAfterStop(Server $server): void
|
||||
{
|
||||
@@ -125,6 +142,9 @@ class CleanupDocker implements ShouldBeUnique
|
||||
$this->deadline = now()->getTimestamp() + self::REMOTE_COMMANDS_DEADLINE;
|
||||
|
||||
if ($onlyAboveThreshold) {
|
||||
if (self::activeDeployment($server)) {
|
||||
return [];
|
||||
}
|
||||
$diskUsage = $server->getDiskUsage();
|
||||
if (is_numeric($diskUsage) && (int) $diskUsage < $server->settings->docker_cleanup_threshold) {
|
||||
return [];
|
||||
|
||||
@@ -110,6 +110,17 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
$activeDeployment = $this->manualCleanup ? null : CleanupDocker::activeDeployment($this->server);
|
||||
if ($activeDeployment) {
|
||||
$this->execution_log->update([
|
||||
'status' => 'success',
|
||||
'message' => 'Docker cleanup skipped: deployment '.$activeDeployment->deployment_uuid.' is queued or running on this server. The next scheduled cleanup runs as usual.',
|
||||
]);
|
||||
event(new DockerCleanupDone($this->execution_log));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
|
||||
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Events\DockerCleanupDone;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -660,3 +661,76 @@ it('keeps an application image name with a single quote inside the grep pattern'
|
||||
expect(file_exists($marker))->toBeFalse()
|
||||
->and(explode("\n", $output))->toBe(['rmi postgres:16']);
|
||||
})->with(['root user' => false, 'non-root user' => true]);
|
||||
|
||||
function dockerCleanupDeployment(Server $server, string $status, string $column = 'server_id', ?Carbon $updatedAt = null): void
|
||||
{
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => '1',
|
||||
'deployment_uuid' => 'deployment-'.str()->random(8),
|
||||
'status' => $status,
|
||||
$column => $server->id,
|
||||
]);
|
||||
if ($updatedAt) {
|
||||
$deployment->forceFill(['updated_at' => $updatedAt])->saveQuietly();
|
||||
}
|
||||
}
|
||||
|
||||
it('skips a forced scheduled cleanup while a deployment is queued or running on the server, so the build cache is not pruned', function (string $status, string $column) {
|
||||
Notification::fake();
|
||||
Event::fake([DockerCleanupDone::class]);
|
||||
$server = dockerCleanupReachableServer();
|
||||
$server->settings()->update(['force_docker_cleanup' => true]);
|
||||
dockerCleanupDeployment($server, $status, $column);
|
||||
$commands = dockerCleanupFakeProcesses();
|
||||
|
||||
(new DockerCleanupJob($server->fresh()))->handle();
|
||||
|
||||
$execution = DockerCleanupExecution::query()->sole();
|
||||
expect($commands->contains(fn (string $command) => str_contains($command, 'prune')))->toBeFalse()
|
||||
->and($execution->status)->toBe('success')
|
||||
->and($execution->message)->toContain('skipped')
|
||||
->and($execution->finished_at)->not->toBeNull();
|
||||
})->with([
|
||||
'running on the server' => ['in_progress', 'server_id'],
|
||||
'queued on the server' => ['queued', 'server_id'],
|
||||
'building on the server' => ['in_progress', 'build_server_id'],
|
||||
]);
|
||||
|
||||
it('runs a forced scheduled cleanup when the only deployments are finished, on another server, or stuck', function () {
|
||||
Notification::fake();
|
||||
Event::fake([DockerCleanupDone::class]);
|
||||
$server = dockerCleanupReachableServer();
|
||||
$server->settings()->update(['force_docker_cleanup' => true, 'dynamic_timeout' => 3600]);
|
||||
dockerCleanupDeployment($server, 'finished');
|
||||
dockerCleanupDeployment(dockerCleanupReachableServer(), 'in_progress');
|
||||
dockerCleanupDeployment($server, 'in_progress', updatedAt: now()->subSeconds(3601));
|
||||
$commands = dockerCleanupFakeProcesses();
|
||||
|
||||
(new DockerCleanupJob($server->fresh()))->handle();
|
||||
|
||||
expect($commands->contains(fn (string $command) => str_contains($command, 'docker builder prune')))->toBeTrue()
|
||||
->and(DockerCleanupExecution::query()->sole()->message)->toContain('Forced Docker cleanup');
|
||||
});
|
||||
|
||||
it('runs a manual cleanup while a deployment is running, because the user started it', function () {
|
||||
Notification::fake();
|
||||
Event::fake([DockerCleanupDone::class]);
|
||||
$server = dockerCleanupReachableServer();
|
||||
dockerCleanupDeployment($server, 'in_progress');
|
||||
$commands = dockerCleanupFakeProcesses();
|
||||
|
||||
(new DockerCleanupJob($server->fresh(), true))->handle();
|
||||
|
||||
expect($commands->contains(fn (string $command) => str_contains($command, 'docker builder prune')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not clean after a stop while a deployment is running on the server', function () {
|
||||
$server = dockerCleanupReachableServer();
|
||||
$server->settings()->update(['docker_cleanup_threshold' => 10]);
|
||||
dockerCleanupDeployment($server, 'in_progress');
|
||||
$commands = dockerCleanupFakeProcesses();
|
||||
|
||||
expect(CleanupDocker::run($server->fresh(), false, false, true))->toBe([])
|
||||
->and($commands)->toBeEmpty()
|
||||
->and(Cache::has(CleanupDocker::lastRunCacheKey($server)))->toBeFalse();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user