fix(backups): support long-running volume backups

This commit is contained in:
Andras Bacsai
2026-08-18 14:10:23 +02:00
parent ad3e7350c1
commit dd597d1527
7 changed files with 76 additions and 6 deletions
@@ -34,7 +34,7 @@ use RuntimeException;
new OA\Property(property: 'retention_amount_s3', type: 'integer', default: 7, minimum: 0, maximum: 10000),
new OA\Property(property: 'retention_days_s3', type: 'integer', default: 0, maximum: 2147483647, minimum: 0),
new OA\Property(property: 'retention_max_storage_s3', type: 'number', format: 'float', default: 0, maximum: 9999999999, minimum: 0),
new OA\Property(property: 'timeout', type: 'integer', default: 3600, minimum: 60, maximum: 36000),
new OA\Property(property: 'timeout', type: 'integer', default: ScheduledVolumeBackup::DEFAULT_TIMEOUT, minimum: 60, maximum: 36000),
],
type: 'object',
additionalProperties: false,
@@ -275,7 +275,7 @@ class VolumeBackupsController extends Controller
'retention_amount_s3' => $request->integer('retention_amount_s3', 7),
'retention_days_s3' => $request->integer('retention_days_s3'),
'retention_max_storage_s3' => $request->float('retention_max_storage_s3'),
'timeout' => $request->integer('timeout', 3600),
'timeout' => $request->integer('timeout', ScheduledVolumeBackup::DEFAULT_TIMEOUT),
]);
$created = $backup->wasRecentlyCreated;
+2 -2
View File
@@ -28,14 +28,14 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
public int $maxExceptions = 1;
public int $timeout = 3600;
public int $timeout = ScheduledVolumeBackup::DEFAULT_TIMEOUT;
private ?ScheduledVolumeBackupExecution $execution = null;
public function __construct(public ScheduledVolumeBackup $backup)
{
$this->onQueue(crons_queue());
$this->timeout = $backup->timeout ?? 3600;
$this->timeout = $backup->timeout ?? ScheduledVolumeBackup::DEFAULT_TIMEOUT;
}
public function middleware(): array
@@ -56,7 +56,7 @@ class VolumeBackups extends Component
public string $timezone = '';
public int $timeout = 3600;
public int $timeout = ScheduledVolumeBackup::DEFAULT_TIMEOUT;
public int $perPage = 10;
+2
View File
@@ -11,6 +11,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
class ScheduledVolumeBackup extends BaseModel
{
public const int DEFAULT_TIMEOUT = 36000;
protected $fillable = [
'uuid',
'backupable_type',
+5 -1
View File
@@ -1,5 +1,6 @@
<?php
use App\Models\ScheduledVolumeBackup;
use Illuminate\Support\Str;
return [
@@ -202,7 +203,10 @@ return [
'tries' => 1,
'nice' => 0,
'sleep' => 3,
'timeout' => env('HORIZON_TIMEOUT', 36000),
'timeout' => min(
max((int) env('HORIZON_TIMEOUT', 39600), ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600),
85800,
),
],
],
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
DB::table('scheduled_volume_backups')
->where('timeout', 3600)
->update(['timeout' => 36000]);
Schema::table('scheduled_volume_backups', function (Blueprint $table) {
$table->unsignedInteger('timeout')->default(36000)->change();
});
}
public function down(): void
{
Schema::table('scheduled_volume_backups', function (Blueprint $table) {
$table->unsignedInteger('timeout')->default(3600)->change();
});
}
};
+37
View File
@@ -51,6 +51,43 @@ it('provides the volume backup domain classes and relationship', function () {
->and(method_exists(LocalFileVolume::class, 'scheduledBackups'))->toBeTrue();
});
it('allows large volume backups to run for ten hours by default', function () {
$backup = new ScheduledVolumeBackup;
$job = new VolumeBackupJob($backup);
expect($job->timeout)->toBe(36000)
->and((new VolumeBackups)->timeout)->toBe(36000)
->and(config('horizon.defaults.s6.timeout'))->toBeGreaterThan($job->timeout)
->and(config('queue.connections.redis.retry_after'))->toBeGreaterThan(config('horizon.defaults.s6.timeout'));
});
it('upgrades existing default volume backup timeouts without changing custom timeouts', function () {
$team = Team::factory()->create();
[$application, $defaultVolume] = createVolumeBackupApplication($team);
$customVolume = LocalPersistentVolume::create([
'name' => 'custom-timeout-data',
'mount_path' => '/custom-data',
'resource_id' => $application->id,
'resource_type' => $application->getMorphClass(),
]);
$defaultBackup = $defaultVolume->scheduledBackups()->create([
'team_id' => $team->id,
'frequency' => 'daily',
'timeout' => 3600,
]);
$customBackup = $customVolume->scheduledBackups()->create([
'team_id' => $team->id,
'frequency' => 'daily',
'timeout' => 7200,
]);
$migration = require database_path('migrations/2026_08_15_000000_increase_default_volume_backup_timeout.php');
$migration->up();
expect($defaultBackup->fresh()->timeout)->toBe(36000)
->and($customBackup->fresh()->timeout)->toBe(7200);
});
it('includes parallel gzip support in the Coolify helper image', function () {
$dockerfile = file_get_contents(base_path('docker/coolify-helper/Dockerfile'));