From b794da098358975608815b7bf7e940560e42702b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:20:35 +0200 Subject: [PATCH] fix(backups): set finished_at for each database of a backup run In a backup of several databases, only the last execution got finished_at. - Set finished_at when each database backup succeeds or fails --- app/Jobs/DatabaseBackupJob.php | 2 + .../DatabaseBackupPerDatabaseStateTest.php | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index b0117a4039..2eba0216de 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -464,6 +464,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue 'size' => $size, 'filename' => null, 's3_uploaded' => null, + 'finished_at' => Carbon::now()->toImmutable(), ]); } try { @@ -512,6 +513,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue 'size' => $size, 's3_uploaded' => $this->backup->save_s3 ? $this->s3_uploaded : null, 'local_storage_deleted' => $localStorageDeleted, + 'finished_at' => Carbon::now()->toImmutable(), ]); // Send appropriate notification (wrapped in try-catch so notification diff --git a/tests/Feature/DatabaseBackupPerDatabaseStateTest.php b/tests/Feature/DatabaseBackupPerDatabaseStateTest.php index 24e318c712..65821b03b5 100644 --- a/tests/Feature/DatabaseBackupPerDatabaseStateTest.php +++ b/tests/Feature/DatabaseBackupPerDatabaseStateTest.php @@ -73,3 +73,43 @@ it('keeps the successful execution of an earlier database when creating the next ->and($first->filename)->not->toBeNull() ->and($commands->contains(fn (string $command) => str_contains($command, 'rm ') && str_contains($command, $first->filename)))->toBeFalse(); }); + +it('finishes the execution of every database in a run with several databases', function () { + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id, + ]); + $server->settings()->update(['is_reachable' => true, 'is_usable' => true]); + $destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail(); + $environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $team->id])->id]); + $database = create_standalone_postgresql($environment->id, $destination, ['status' => 'running:healthy']); + $backup = new ScheduledDatabaseBackup; + $backup->forceFill([ + 'enabled' => true, + 'save_s3' => false, + 'frequency' => 'daily', + 'databases_to_backup' => 'first,second,third', + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => $team->id, + ])->save(); + Process::fake(function ($process) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + if (str_contains($command, 'pg_dump') && str_contains($command, 'second')) { + return Process::result(errorOutput: 'pg_dump: database "second" does not exist', exitCode: 1); + } + + return Process::result(output: str_contains($command, 'du -b') ? '128' : ''); + }); + + (new DatabaseBackupJob($backup->fresh()))->handle(); + + $executions = ScheduledDatabaseBackupExecution::query()->orderBy('id')->get(); + expect($executions->map->only(['database_name', 'status'])->all())->toBe([ + ['database_name' => 'first', 'status' => 'success'], + ['database_name' => 'second', 'status' => 'failed'], + ['database_name' => 'third', 'status' => 'success'], + ]) + ->and($executions->whereNull('finished_at')->pluck('database_name')->all())->toBe([]); +});