| package restore |
| |
| import ( |
| "fmt" |
| "runtime" |
| "strconv" |
| "strings" |
| |
| "github.com/apache/cloudberry-backup/options" |
| "github.com/apache/cloudberry-go-libs/cluster" |
| "github.com/apache/cloudberry-go-libs/gplog" |
| "github.com/apache/cloudberry-go-libs/iohelper" |
| "github.com/pkg/errors" |
| ) |
| |
| /* |
| * Functions to run commands on entire cluster during restore |
| */ |
| |
| func VerifyBackupDirectoriesExistOnAllHosts() { |
| _, err := globalCluster.ExecuteLocalCommand(fmt.Sprintf("test -d %s", globalFPInfo.GetDirForContent(-1))) |
| gplog.FatalOnError(err, "Backup directory %s missing or inaccessible", globalFPInfo.GetDirForContent(-1)) |
| if MustGetFlagString(options.PLUGIN_CONFIG) == "" || backupConfig.SingleDataFile { |
| origSize, destSize, isResizeRestore, _ := GetResizeClusterInfo() |
| |
| remoteOutput := globalCluster.GenerateAndExecuteCommand("Verifying backup directories exist", cluster.ON_SEGMENTS, func(contentID int) string { |
| if isResizeRestore { // Map origin content to destination content to find where the original files have been placed |
| if contentID >= origSize { // Don't check for directories for contents that aren't part of the backup set |
| return "" |
| } |
| contentID = contentID % destSize |
| } |
| return fmt.Sprintf("test -d %s", globalFPInfo.GetDirForContent(contentID)) |
| }) |
| globalCluster.CheckClusterError(remoteOutput, "Backup directories missing or inaccessible", func(contentID int) string { |
| return fmt.Sprintf("Backup directory %s missing or inaccessible", globalFPInfo.GetDirForContent(contentID)) |
| }) |
| } |
| } |
| |
| func VerifyBackupFileCountOnSegments() { |
| // In the current backup directory format, all content IDs are intermingled in one directory, so we need to get a list of which contents |
| // correspond to the content ID we're going to check in order to provide a useful count to the user in the case of an error. |
| origSize, destSize, isResizeRestore, _ := GetResizeClusterInfo() |
| contentMap := make(map[int][]string, destSize) // []string instead of []int so we can join them later |
| for i := 0; i < origSize; i++ { |
| contentMap[i%destSize] = append(contentMap[i%destSize], fmt.Sprintf("%d", i)) |
| } |
| |
| remoteOutput := globalCluster.GenerateAndExecuteCommand("Verifying backup file count", cluster.ON_SEGMENTS, func(contentID int) string { |
| // Coordinator backup files (and any gprestore report files) will be mixed in with segment backup files on a single-node cluster, |
| // so we explicitly look for filenames in the segment filename format. In a smaller-to-larger restore, the contents list for a segment |
| // outside the destination array will be "[]", which the find command can handle safely in this context. |
| contentsList := fmt.Sprintf("(%s)", strings.Join(contentMap[contentID], "|")) |
| var cmdString string |
| if runtime.GOOS == "linux" { |
| cmdString = fmt.Sprintf(`find %s -type f -regextype posix-extended -regex ".*gpbackup_%s_%s.*" | wc -l`, globalFPInfo.GetDirForContent(contentID), contentsList, globalFPInfo.Timestamp) |
| } else if runtime.GOOS == "darwin" { |
| cmdString = fmt.Sprintf(`find -E %s -type f -regex ".*gpbackup_%s_%s.*" | wc -l`, globalFPInfo.GetDirForContent(contentID), contentsList, globalFPInfo.Timestamp) |
| } |
| return cmdString |
| }) |
| globalCluster.CheckClusterError(remoteOutput, "Could not verify backup file count", func(contentID int) string { |
| return "Could not verify backup file count" |
| }) |
| |
| // these are the file counts for non-resize restores. |
| fileCount := 2 // 1 for the actual data file, 1 for the segment TOC file |
| if !backupConfig.SingleDataFile { |
| fileCount = len(globalTOC.DataEntries) |
| } |
| |
| batchMap := make(map[int]int, len(remoteOutput.Commands)) |
| for i := 0; i < origSize; i++ { |
| batchMap[i%destSize] += fileCount |
| } |
| |
| numIncorrect := 0 |
| for contentID, cmd := range remoteOutput.Commands { |
| numFound, _ := strconv.Atoi(strings.TrimSpace(cmd.Stdout)) |
| if isResizeRestore { |
| fileCount = batchMap[contentID] |
| } |
| if numFound != fileCount { |
| gplog.Verbose("Expected to find %d file(s) on segment %d on host %s, but found %d instead.", fileCount, contentID, globalCluster.GetHostForContent(contentID), numFound) |
| numIncorrect++ |
| } |
| } |
| if numIncorrect > 0 { |
| cluster.LogFatalClusterError("Found incorrect number of backup files", cluster.ON_SEGMENTS, numIncorrect) |
| } |
| } |
| |
| func VerifyMetadataFilePaths(withStats bool) { |
| filetypes := []string{"config", "table of contents", "metadata"} |
| missing := false |
| for _, filetype := range filetypes { |
| filepath := globalFPInfo.GetBackupFilePath(filetype) |
| if !iohelper.FileExistsAndIsReadable(filepath) { |
| missing = true |
| gplog.Error("Cannot access %s file %s", filetype, filepath) |
| } |
| } |
| if withStats { |
| filepath := globalFPInfo.GetStatisticsFilePath() |
| if !iohelper.FileExistsAndIsReadable(filepath) { |
| missing = true |
| gplog.Error("Cannot access statistics file %s", filepath) |
| gplog.Error(`Note that the "-with-stats" flag must be passed to gpbackup to generate a statistics file.`) |
| } |
| } |
| if missing { |
| gplog.Fatal(errors.Errorf("One or more metadata files do not exist or are not readable."), "Cannot proceed with restore") |
| } |
| } |
| |
| func GetResizeClusterInfo() (int, int, bool, int) { |
| isResizeCluster := MustGetFlagBool(options.RESIZE_CLUSTER) |
| origSize := backupConfig.SegmentCount |
| destSize := len(globalCluster.ContentIDs) - 1 |
| if !isResizeCluster && origSize == 0 { // Backup taken with version <1.26, no SegmentCount stored |
| origSize = destSize |
| } |
| |
| batches := 1 |
| if isResizeCluster && origSize > destSize { |
| batches = origSize / destSize |
| // If dest doesn't divide evenly into orig, there's one more incomplete batch |
| if origSize%destSize != 0 { |
| batches += 1 |
| } |
| } |
| |
| return origSize, destSize, isResizeCluster, batches |
| } |