Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions pkg/backup/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ var (
ErrBackupIsAlreadyExists = errors.New("backup is already exists")
)

func (b *Backuper) resumeExistingBackup(backupName string) error {
_, checkDownloadErr := os.Stat(path.Join(b.DefaultDataPath, "backup", backupName, "download.state2"))
if errors.Is(checkDownloadErr, os.ErrNotExist) {
// wrap ErrBackupIsAlreadyExists so RestoreFromRemote keeps reusing an already complete local backup (issue #625),
// while a standalone `download --resume` surfaces the guidance below instead of a bare "backup is already exists"
return fmt.Errorf("%w: local backup '%s' exists but resumable state 'download.state2' is missing, so it is unknown which parts are complete and resuming on top of partial data risks silent corruption; run `clickhouse-backup delete local %s` and retry the download, or investigate why the resumable state was lost", ErrBackupIsAlreadyExists, backupName, backupName)
}
log.Warn().Msgf("%s already exists will try to resume download", backupName)
return nil
}

func (b *Backuper) Download(backupName string, tablePattern string, partitions []string, schemaOnly, rbacOnly, configsOnly, namedCollectionsOnly, resume bool, hardlinkExistsFiles bool, backupVersion string, commandId int) error {
if pidCheckErr := pidlock.CheckAndCreatePidFile(backupName, "download"); pidCheckErr != nil {
return errors.WithMessage(pidCheckErr, "CheckAndCreatePidFile")
Expand Down Expand Up @@ -88,14 +99,11 @@ func (b *Backuper) Download(backupName string, tablePattern string, partitions [
}
if !b.resume {
return ErrBackupIsAlreadyExists
} else {
_, checkDownloadErr := os.Stat(path.Join(b.DefaultDataPath, "backup", backupName, "download.state2"))
if errors.Is(checkDownloadErr, os.ErrNotExist) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better return here following error

local backup '<name>' exists but download.state2 is missing.
  run `clickhouse-backup delete local <name>` and retry,
  or investigate why the resumable state was lost.

return ErrBackupIsAlreadyExists
}
isResumeExists = true
log.Warn().Msgf("%s already exists will try to resume download", backupName)
}
if resumeErr := b.resumeExistingBackup(backupName); resumeErr != nil {
return resumeErr
}
isResumeExists = true
}
}
startDownload := time.Now()
Expand Down
30 changes: 26 additions & 4 deletions pkg/backup/download_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package backup

import (
"os"
"path"
"regexp"
"testing"
"time"
Expand All @@ -10,6 +12,7 @@ import (
"github.com/Altinity/clickhouse-backup/v2/pkg/metadata"
"github.com/Altinity/clickhouse-backup/v2/pkg/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var b = Backuper{
Expand Down Expand Up @@ -91,6 +94,25 @@ var remoteBackup = storage.Backup{
UploadDate: time.Now(),
}

func TestResumeExistingBackupMissingStateFileReturnsError(t *testing.T) {
backupName := "test_resume_crash"
defaultDataPath := t.TempDir()
backupDir := path.Join(defaultDataPath, "backup", backupName)
assert.NoError(t, os.MkdirAll(backupDir, 0o750))

backuper := &Backuper{DefaultDataPath: defaultDataPath}

err := backuper.resumeExistingBackup(backupName)
require.Error(t, err)
assert.Contains(t, err.Error(), "download.state2")
assert.Contains(t, err.Error(), "delete local "+backupName)
// must keep wrapping ErrBackupIsAlreadyExists so RestoreFromRemote reuses the local backup (issue #625)
assert.ErrorIs(t, err, ErrBackupIsAlreadyExists)

assert.NoError(t, os.WriteFile(path.Join(backupDir, "download.state2"), []byte("state"), 0o640))
assert.NoError(t, backuper.resumeExistingBackup(backupName))
}

func TestReBalanceTablesMetadataIfDiskNotExists_Files_NoErrors(t *testing.T) {
remoteBackup.DataFormat = "tar"
baseTable := metadata.TableMetadata{
Expand Down Expand Up @@ -234,7 +256,7 @@ func TestReBalanceTablesMetadataIfDiskNotExists_CheckErrors(t *testing.T) {
tableMetadataAfterDownloadRepacked[i] = tableMetadataAfterDownload[i]
}
err := b.reBalanceTablesMetadataIfDiskNotExists(tableMetadataAfterDownloadRepacked, baseDisks, invalidRemoteBackup)
assert.Error(t, err)
require.Error(t, err)
assert.Contains(t, err.Error(),
"disk: hdd2 not found in disk_types section map[string]string{\"default\":\"local\", \"s3\":\"s3\", \"s3_disk2\":\"s3\"} in Test/metadata.json",
)
Expand All @@ -246,7 +268,7 @@ func TestReBalanceTablesMetadataIfDiskNotExists_CheckErrors(t *testing.T) {
tableMetadataAfterDownloadRepacked[i] = tableMetadataAfterDownload[i]
}
err = b.reBalanceTablesMetadataIfDiskNotExists(tableMetadataAfterDownloadRepacked, baseDisks, invalidRemoteBackup)
assert.Error(t, err)
require.Error(t, err)
assert.Contains(t, err.Error(), "disk: hdd2, diskType: unknown not found in system.disks")

// invalid storage_policy
Expand All @@ -262,7 +284,7 @@ func TestReBalanceTablesMetadataIfDiskNotExists_CheckErrors(t *testing.T) {
invalidTable.Query = "CREATE TABLE default.test3(id UInt64) ENGINE=MergeTree() ORDER BY id SETTINGS storage_policy='invalid'"
tableMetadataAfterDownloadRepacked = ListOfTables{&invalidTable}
err = b.reBalanceTablesMetadataIfDiskNotExists(tableMetadataAfterDownloadRepacked, baseDisks, invalidRemoteBackup)
assert.Error(t, err)
require.Error(t, err)
matched, matchErr := regexp.MatchString(`storagePolicy: invalid with diskType: \w+ not found in system.disks`, err.Error())
assert.NoError(t, matchErr)
assert.True(t, matched)
Expand All @@ -278,7 +300,7 @@ func TestReBalanceTablesMetadataIfDiskNotExists_CheckErrors(t *testing.T) {
}
tableMetadataAfterDownloadRepacked = ListOfTables{&invalidTable}
err = b.reBalanceTablesMetadataIfDiskNotExists(tableMetadataAfterDownloadRepacked, invalidDisks, invalidRemoteBackup)
assert.Error(t, err)
require.Error(t, err)
assert.Contains(t, err.Error(), "250B free space, not found in system.disks with `local` type")

}