diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b650cf55..7fbc0dfe47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed +- Finish the startup and the nightly cleanup of stuck resources on instances with many large deployment logs. Before, the cleanup loaded the logs of all deployments and stopped with "Allowed memory size exhausted", so stuck deployments, tasks, and backups were not marked as failed and the proxy configuration was not set up ([#12435](https://github.com/coollabsio/coolify/issues/12435)) +- Open the database backup pages and **Settings > Backup** faster when a backup has many executions. Before, these pages loaded all executions of the backup on each request, but they did not show them - Queue a deployment from the MCP `deploy` tool and the MCP `control` start action while a deployment of the same application is in progress, as the API and the **Deploy** button do. Before, both started at once, so two rolling updates could remove the new container of each other and stop the application. A deployment of the same commit is now skipped, and the response gives the UUID of the deployment in progress. A rolling update now also removes only the containers that ran before it started ([#12442](https://github.com/coollabsio/coolify/issues/12442)) - Do not copy the preview deployments of an application when you clone it. A clone of an application with a fixed preview URL template (for example `demo-mr-{{pr_id}}.example.com`) failed with a duplicate preview domain error and left a stopped, partial clone. The clone gets its own previews from new pull requests - Handle an existing Cloudflare DNS record with a different IP when Coolify adds the DNS record of a new domain automatically. Coolify now replaces the record when it created the record and no other resource uses it. Otherwise it opens the **Configure DNS** dialog, where you can replace the record, and the domain shows the conflict instead of "Not checked" diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index cc7258d6b6..3b2ca57599 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -70,7 +70,9 @@ class CleanupStuckedResources extends Command echo "Error in cleaning stuck servers: {$e->getMessage()}\n"; } try { - $applicationsDeploymentQueue = ApplicationDeploymentQueue::query()->lazyById(); + $applicationsDeploymentQueue = ApplicationDeploymentQueue::query() + ->select(['id', 'application_id']) + ->lazyById(); foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) { if (is_null($applicationDeploymentQueue->application)) { echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n"; diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 02be98fc9a..9dc0bc0cec 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -120,8 +120,7 @@ class Emails extends Command case 'application-deployment-success-daily': $applications = Application::all(); foreach ($applications as $application) { - $deployments = $application->get_last_days_deployments(); - if ($deployments->isEmpty()) { + if (! $application->hasDeploymentsInLastDays()) { continue; } $this->mail = (new DeploymentSuccess($application, 'test'))->toMail(); diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php index c96c7e5217..063ddbe531 100644 --- a/app/Livewire/Project/Database/Backup/Execution.php +++ b/app/Livewire/Project/Database/Backup/Execution.php @@ -23,8 +23,6 @@ class Execution extends Component public ?ScheduledDatabaseBackup $backup; - public $executions; - public $s3s; public array $parameters = []; @@ -73,10 +71,8 @@ class Execution extends Component if (! $backup) { return redirect()->route('dashboard'); } - $executions = collect($backup->executions)->sortByDesc('created_at'); $this->database = $database; $this->backup = $backup; - $this->executions = $executions; $this->s3s = S3Storage::query() ->where('team_id', $database->team()->id) ->where('is_usable', true) diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index d7eaf7d596..7782ce2598 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -28,9 +28,6 @@ class SettingsBackup extends Component #[Locked] public $s3s; - #[Locked] - public $executions = []; - #[Validate(['nullable'])] public ?string $description = null; @@ -58,7 +55,6 @@ class SettingsBackup extends Component $this->backup->enabled = false; $this->backup->save(); } - $this->executions = $this->backup?->executions ?? []; } $this->settings = $settings; $this->is_backup_before_update_enabled = $settings->is_backup_before_update_enabled; @@ -112,7 +108,6 @@ class SettingsBackup extends Component $this->s3s = S3Storage::whereTeamId(0)->get(); $this->description = $this->database->description; - $this->executions = $this->backup->executions; } catch (\Exception $e) { return handleError($e, $this); diff --git a/app/Models/Application.php b/app/Models/Application.php index 9b0b735950..96d7389471 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1323,9 +1323,9 @@ class Application extends BaseModel return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED->value)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first($columns); } - public function get_last_days_deployments() + public function hasDeploymentsInLastDays(int $days = 7): bool { - return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays($days))->exists(); } /** diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 3c31c0fbc7..c1c21106ef 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -87,11 +87,6 @@ class ScheduledDatabaseBackup extends BaseModel return $this->belongsToMany(S3Storage::class, 'scheduled_database_backup_s3_storage')->withTimestamps(); } - public function get_last_days_backup_status($days = 7) - { - return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); - } - public function executionsPaginated(int $skip = 0, int $take = 10) { $executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->with('s3Replicas.s3')->orderBy('created_at', 'desc'); diff --git a/tests/Feature/CleanupStuckedResourcesTest.php b/tests/Feature/CleanupStuckedResourcesTest.php index 24229cf3d1..0d8f20ec9c 100644 --- a/tests/Feature/CleanupStuckedResourcesTest.php +++ b/tests/Feature/CleanupStuckedResourcesTest.php @@ -1,6 +1,7 @@ fresh())->not->toBeNull() ->and($orphanedTask->fresh())->toBeNull(); }); + +it('deletes orphaned deployments without loading their logs', function () { + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + Queue::fake(); + + $team = Team::factory()->create(); + $server = Server::factory()->create(['team_id' => $team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = $project->environments()->first() + ?? Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + $validDeployment = ApplicationDeploymentQueue::create([ + 'application_id' => $application->id, + 'deployment_uuid' => 'valid-deployment', + 'server_id' => $server->id, + 'logs' => str_repeat('x', 1000), + ]); + $orphanedDeployment = ApplicationDeploymentQueue::create([ + 'application_id' => PHP_INT_MAX, + 'deployment_uuid' => 'orphaned-deployment', + 'server_id' => $server->id, + 'logs' => str_repeat('x', 1000), + ]); + + DB::enableQueryLog(); + $this->artisan('cleanup:stucked-resources')->assertSuccessful(); + $deploymentSelects = collect(DB::getQueryLog()) + ->pluck('query') + ->filter(fn (string $query) => str_starts_with($query, 'select') && str_contains($query, 'from "application_deployment_queues"')); + + expect($validDeployment->fresh())->not->toBeNull() + ->and($orphanedDeployment->fresh())->toBeNull() + ->and($deploymentSelects)->not->toBeEmpty() + ->and($deploymentSelects->filter(fn (string $query) => str_contains($query, '*')))->toBeEmpty(); +}); diff --git a/tests/Feature/SettingsBackupEmptyStateTest.php b/tests/Feature/SettingsBackupEmptyStateTest.php index f9c4ef9faf..ed7cb3aa02 100644 --- a/tests/Feature/SettingsBackupEmptyStateTest.php +++ b/tests/Feature/SettingsBackupEmptyStateTest.php @@ -136,7 +136,6 @@ test('instance backup settings render when the instance database has no backup s Livewire::test(SettingsBackup::class) ->assertOk() - ->assertSet('executions', []) ->assertSee('Backup is not configured') ->assertSee('Configure backup'); });