diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 0cb2b4d9b0..b0117a4039 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -323,6 +323,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->backup_dir = backup_dir().'/coolify'."/coolify-db-$ip"; } foreach ($databasesToBackup as $database) { + // Reset per-database state, so a failure before this database's execution exists + // does not mark the previous database's execution failed or delete its backup file. + $this->backup_log = null; + $this->backup_location = null; + $this->backup_output = null; + $this->error_output = null; + // Generate unique UUID for each database backup execution $attempts = 0; do { diff --git a/tests/Feature/DatabaseBackupPerDatabaseStateTest.php b/tests/Feature/DatabaseBackupPerDatabaseStateTest.php new file mode 100644 index 0000000000..24e318c712 --- /dev/null +++ b/tests/Feature/DatabaseBackupPerDatabaseStateTest.php @@ -0,0 +1,75 @@ + InstanceSettings::firstOrCreate(['id' => 0])); +}); + +afterEach(function () { + ScheduledDatabaseBackupExecution::flushEventListeners(); +}); + +it('keeps the successful execution of an earlier database when creating the next execution fails', 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', + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => $team->id, + ])->save(); + $commands = collect(); + Process::fake(function ($process) use ($commands) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + $commands->push($command); + + return Process::result(output: str_contains($command, 'du -b') ? '128' : ''); + }); + ScheduledDatabaseBackupExecution::creating(function (ScheduledDatabaseBackupExecution $execution) { + if ($execution->database_name === 'second') { + throw new RuntimeException('database is unavailable'); + } + }); + + (new DatabaseBackupJob($backup->fresh()))->handle(); + + $first = ScheduledDatabaseBackupExecution::query()->sole(); + expect($first->database_name)->toBe('first') + ->and($first->status)->toBe('success') + ->and($first->filename)->not->toBeNull() + ->and($commands->contains(fn (string $command) => str_contains($command, 'rm ') && str_contains($command, $first->filename)))->toBeFalse(); +});