fix(backups): accept tar exit code 1 in volume backups

GNU tar exits with 1 when a file changes while it is read. The archive
is still valid, so only exit codes above 1 fail the backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-16 22:41:15 +02:00
co-authored by Claude Opus 5.5
parent 44774f7b93
commit 449d099bd3
2 changed files with 29 additions and 3 deletions
+10 -3
View File
@@ -92,8 +92,7 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
? 'docker volume inspect '.escapeshellarg($source).' >/dev/null'
: 'test -d '.escapeshellarg($source);
$compressorCommand = BackupCompression::compressorCommand($compressionCpuPercentage);
$archiveScript = "compressor=\$({$compressorCommand}); tar -I \"\$compressor\" -cf - -C /volume .";
$archiveScript = self::archiveScript($compressionCpuPercentage);
if ($streamToS3) {
$this->execution->update(['local_storage_deleted' => true]);
$archiveCommand = $this->streamToS3Command($archiveScript, $backupLocation, $source, $containerName, $image);
@@ -289,8 +288,16 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
}
/**
* @return array<int, string>
* GNU tar exits with 1 when a file changes while it is read (for example, an app that writes to the volume
* during a live backup). The archive is still valid, so only exit codes above 1 fail the backup.
*/
public static function archiveScript(int $compressionCpuPercentage): string
{
$compressorCommand = BackupCompression::compressorCommand($compressionCpuPercentage);
return "compressor=\$({$compressorCommand}); { tar -I \"\$compressor\" -cf - -C /volume . || { status=\$?; [ \$status -eq 1 ] || exit \$status; }; }";
}
/**
* Containers can exit and be removed (e.g. `--rm` backup helpers) between `docker ps` and
* `docker inspect`. Skip those, but still fail when an existing container cannot be inspected.
+19
View File
@@ -2969,3 +2969,22 @@ it('records a missed volume backup that is too old to run late as a failed execu
Notification::assertSentToTimes($team, BackupFailed::class, 1);
Notification::assertSentTo($team, BackupFailed::class, fn (BackupFailed $notification) => $notification->output === 'Skipped: the queued job did not start within 60 minutes.');
});
it('fails the volume archive only on fatal tar errors', function (int $tarExitCode, int $expectedExitCode) {
$bin = sys_get_temp_dir().'/volume-backup-tar-'.uniqid();
mkdir($bin);
file_put_contents($bin.'/tar', "#!/bin/sh\nexit {$tarExitCode}\n");
chmod($bin.'/tar', 0755);
$result = Process::env(['PATH' => $bin.':/usr/bin:/bin'])
->run(['sh', '-c', VolumeBackupJob::archiveScript(50)]);
unlink($bin.'/tar');
rmdir($bin);
expect($result->exitCode())->toBe($expectedExitCode);
})->with([
'success' => [0, 0],
'file changed as we read it' => [1, 0],
'fatal error' => [2, 2],
]);