blob: aab33f784450437094b8e2627c5e3faf1dcbdd5a [file]
package utils
import (
"fmt"
"io"
path "path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/apache/cloudberry-backup/filepath"
"github.com/apache/cloudberry-go-libs/cluster"
"github.com/apache/cloudberry-go-libs/gplog"
"github.com/apache/cloudberry-go-libs/iohelper"
"github.com/apache/cloudberry-go-libs/operating"
"github.com/pkg/errors"
)
var helperMutex sync.Mutex
/*
* Functions to run commands on entire cluster during both backup and restore
*/
/*
* The reason that gprestore is in charge of creating the first pipe to ensure
* that the first pipe is created before the first COPY FROM is issued. If
* gpbackup_helper was in charge of creating the first pipe, there is a
* possibility that the COPY FROM commands start before gpbackup_helper is done
* starting up and setting up the first pipe.
*/
func CreateSegmentPipeOnAllHostsForBackup(oid string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
remoteOutput := c.GenerateAndExecuteCommand("Creating segment data pipes", cluster.ON_SEGMENTS, func(contentID int) string {
pipeName := fpInfo.GetSegmentPipeFilePath(contentID)
pipeName = fmt.Sprintf("%s_%s", pipeName, oid)
gplog.Debug("Creating pipe %s", pipeName)
return fmt.Sprintf("mkfifo -m 0700 %s", pipeName)
})
c.CheckClusterError(remoteOutput, "Unable to create segment data pipes", func(contentID int) string {
return "Unable to create segment data pipe"
})
}
func CreateSegmentPipeOnAllHostsForRestore(oid string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
oidWithBatch := strings.Split(oid, ",")
remoteOutput := c.GenerateAndExecuteCommand("Creating segment data pipes", cluster.ON_SEGMENTS, func(contentID int) string {
pipeName := fpInfo.GetSegmentPipeFilePath(contentID)
pipeName = fmt.Sprintf("%s_%s_%s", pipeName, oidWithBatch[0], oidWithBatch[1])
gplog.Debug("Creating pipe %s", pipeName)
return fmt.Sprintf("mkfifo %s", pipeName)
})
c.CheckClusterError(remoteOutput, "Unable to create segment data pipes", func(contentID int) string {
return "Unable to create segment data pipe"
})
}
func WriteOidListToSegments(oidList []string, c *cluster.Cluster, fpInfo filepath.FilePathInfo, fileSuffix string) {
rsync_exists := CommandExists("rsync")
if !rsync_exists {
gplog.Fatal(errors.New("Failed to find rsync on PATH. Please ensure rsync is installed."), "")
}
localOidFile, err := operating.System.TempFile("", "gpbackup-oids")
gplog.FatalOnError(err, "Cannot open temporary file to write oids")
defer func() {
err = operating.System.Remove(localOidFile.Name())
if err != nil {
gplog.Warn("Cannot remove temporary oid file: %s, Err: %s", localOidFile.Name(), err.Error())
}
}()
WriteOidsToFile(localOidFile.Name(), oidList)
generateScpCmd := func(contentID int) string {
sourceFile := localOidFile.Name()
hostname := c.GetHostForContent(contentID)
dest := fpInfo.GetSegmentHelperFilePath(contentID, fileSuffix)
return fmt.Sprintf(`rsync -e ssh %s %s:%s`, sourceFile, hostname, dest)
}
remoteOutput := c.GenerateAndExecuteCommand("rsync oid file to segments", cluster.ON_LOCAL|cluster.ON_SEGMENTS, generateScpCmd)
errMsg := "Failed to rsync oid file"
errFunc := func(contentID int) string {
return "Failed to run rsync"
}
c.CheckClusterError(remoteOutput, errMsg, errFunc, false)
}
func WriteOidsToFile(filename string, oidList []string) {
oidFp, err := iohelper.OpenFileForWriting(filename)
gplog.FatalOnError(err, filename)
defer func() {
err = oidFp.Close()
gplog.FatalOnError(err, filename)
}()
err = WriteOids(oidFp, oidList)
gplog.FatalOnError(err, filename)
}
func WriteOids(writer io.Writer, oidList []string) error {
var err error
for _, oid := range oidList {
_, err = writer.Write([]byte(oid + "\n"))
if err != nil {
// error logging handled in calling functions
return err
}
}
return nil
}
func VerifyHelperVersionOnSegments(version string, c *cluster.Cluster) {
remoteOutput := c.GenerateAndExecuteCommand("Verifying gpbackup_helper version", cluster.ON_HOSTS, func(contentID int) string {
gphome := operating.System.Getenv("GPHOME")
return fmt.Sprintf("%s/bin/gpbackup_helper --version", gphome)
})
c.CheckClusterError(remoteOutput, "Could not verify gpbackup_helper version", func(contentID int) string {
return "Could not verify gpbackup_helper version"
})
numIncorrect := 0
for contentID, cmd := range remoteOutput.Commands {
parsedSegVersion := ""
segVersion := strings.TrimSpace(cmd.Stdout) // Expected format is "gpbackup_helper version [version string]"
splitSegVersion := strings.Split(segVersion, " ")
if len(splitSegVersion) == 3 {
// Array access placed inside a length guard to keep error messages nice, instead of
// spilling panics to the log
parsedSegVersion = splitSegVersion[2]
}
if parsedSegVersion != version {
gplog.Verbose("Version mismatch for gpbackup_helper on segment %d on host %s: Expected version %s, found version %s.", contentID, c.GetHostForContent(contentID), version, parsedSegVersion)
numIncorrect++
}
}
if numIncorrect > 0 {
cluster.LogFatalClusterError("The version of gpbackup_helper must match the version of gpbackup/gprestore, but found gpbackup_helper binaries with invalid version", cluster.ON_HOSTS, numIncorrect)
}
}
func StartGpbackupHelpers(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string, pluginConfigFile string, compressStr string, onErrorContinue bool, isFilter bool, wasTerminated *bool, copyQueue int, isSingleDataFile bool, resizeCluster bool, origSize int, destSize int, verbosity int) {
// A mutex lock for cleaning up and starting gpbackup helpers prevents a
// race condition that causes gpbackup_helpers to be orphaned if
// gpbackup_helper cleanup happens before they are started.
helperMutex.Lock()
if *wasTerminated {
helperMutex.Unlock()
select {} // Pause forever and wait for cleanup to exit program.
}
defer helperMutex.Unlock()
gphomePath := operating.System.Getenv("GPHOME")
envSourceCommand := SourceClusterEnvCommand(gphomePath)
pluginStr := ""
if pluginConfigFile != "" {
_, configFilename := path.Split(pluginConfigFile)
pluginStr = fmt.Sprintf(" --plugin-config /tmp/%s", configFilename)
}
onErrorContinueStr := ""
if onErrorContinue {
onErrorContinueStr = " --on-error-continue"
}
filterStr := ""
if isFilter {
filterStr = " --with-filters"
}
singleDataFileStr := ""
if isSingleDataFile {
singleDataFileStr = " --single-data-file"
}
resizeStr := ""
if resizeCluster {
resizeStr = fmt.Sprintf(" --resize-cluster --orig-seg-count %d --dest-seg-count %d", origSize, destSize)
}
remoteOutput := c.GenerateAndExecuteCommand("Starting gpbackup_helper agent", cluster.ON_SEGMENTS, func(contentID int) string {
tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
oidFile := fpInfo.GetSegmentHelperFilePath(contentID, "oid")
scriptFile := fpInfo.GetSegmentHelperFilePath(contentID, "script")
pipeFile := fpInfo.GetSegmentPipeFilePath(contentID)
backupFile := fpInfo.GetTableBackupFilePath(contentID, 0, GetPipeThroughProgram().Extension, true)
helperCmdStr := fmt.Sprintf(`gpbackup_helper %s --toc-file %s --oid-file %s --pipe-file %s --data-file "%s" --content %d%s%s%s%s%s%s --copy-queue-size %d --verbosity %d`,
operation, tocFile, oidFile, pipeFile, backupFile, contentID, pluginStr, compressStr, onErrorContinueStr, filterStr, singleDataFileStr, resizeStr, copyQueue, verbosity)
// we run these commands in sequence to ensure that any failure is critical; the last command ensures the agent process was successfully started
return fmt.Sprintf(`cat << HEREDOC > %[1]s && chmod +x %[1]s && ( nohup %[1]s &> /dev/null &)
#!/bin/bash
%[3]s
%[2]s/bin/%[4]s
HEREDOC
`, scriptFile, gphomePath, envSourceCommand, helperCmdStr)
})
c.CheckClusterError(remoteOutput, "Error starting gpbackup_helper agent", func(contentID int) string {
return "Error starting gpbackup_helper agent"
})
}
func findCommandStr(c *cluster.Cluster, fpInfo filepath.FilePathInfo, contentID int) string {
var cmdString string
if runtime.GOOS == "linux" {
cmdString = fmt.Sprintf(`find %s -regextype posix-extended -regex ".*gpbackup_%d_%s_(oid|script|pipe)_%d.*"`,
c.GetDirForContent(contentID), contentID, fpInfo.Timestamp, fpInfo.PID)
} else if runtime.GOOS == "darwin" {
cmdString = fmt.Sprintf(`find -E %s -regex ".*gpbackup_%d_%s_(oid|script|pipe)_%d.*"`,
c.GetDirForContent(contentID), contentID, fpInfo.Timestamp, fpInfo.PID)
}
return cmdString
}
func GetHelperFileCount(c *cluster.Cluster, fpInfo filepath.FilePathInfo) int {
totalFiles := 0
remoteOutput := c.GenerateAndExecuteCommand("Checking for leftover gpbackup_helper files on segments", cluster.ON_SEGMENTS, func(contentID int) string {
return fmt.Sprintf("%s %s", findCommandStr(c, fpInfo, contentID), "| wc -l")
})
if remoteOutput.NumErrors > 0 {
gplog.Error("Unable to check for leftover gpbackup_helper files on segments")
} else {
for _, cmd := range remoteOutput.Commands {
numFiles, _ := strconv.Atoi(strings.TrimSpace(cmd.Stdout))
if numFiles > 0 {
totalFiles = totalFiles + numFiles
}
}
}
return totalFiles
}
// Removes all gpbackup_helper files and pipes from the segment data directories.
// It's expected that gpbackup_helper cleans up the files on exit, but that is not guaranteed,
// so this function is used to clean up any leftover files.
func RemoveHelperFiles(c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
remoteOutput := c.GenerateAndExecuteCommand("Removing gpbackup_helper files from segment data directories", cluster.ON_SEGMENTS, func(contentID int) string {
return fmt.Sprintf("%s %s", findCommandStr(c, fpInfo, contentID), `-exec rm -f {} \;`)
})
errMsg := fmt.Sprintf("Unable to remove gpbackup_helper file(s). See %s for a complete list of segments with errors and remove manually.",
gplog.GetLogFilePath())
c.CheckClusterError(remoteOutput, errMsg, func(contentID int) string {
return fmt.Sprintf("Unable to remove gpbackup_helper file(s)\n\t%s", findCommandStr(c, fpInfo, contentID))
}, true)
}
func CleanUpHelperFilesOnAllHosts(c *cluster.Cluster, fpInfo filepath.FilePathInfo, timeout time.Duration) {
var fileCount int
helperMutex.Lock()
defer helperMutex.Unlock()
tickerCleanup := time.NewTicker(1 * time.Second)
for {
fileCount = GetHelperFileCount(c, fpInfo)
select {
case <-tickerCleanup.C:
if fileCount == 0 {
return
}
RemoveHelperFiles(c, fpInfo)
case <-time.After(timeout):
gplog.Warn("Timeout of %ds reached while waiting for %d gpbackup_helper file(s) to be removed.", int(timeout.Seconds()), fileCount)
return
}
}
}
// Checks for gpbackup_helper processes on segments.
// Returns a boolean indicating whether any PIDs were found and a map of hostnames to PIDs.
func CheckHelperPids(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string) (map[string][]int, bool) {
var foundPid bool = false
remoteOutput := c.GenerateAndExecuteCommand("Checking for leftover gpbackup_helper processes", cluster.ON_SEGMENTS, func(contentID int) string {
tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
procPattern := fmt.Sprintf("gpbackup_helper --%s-agent --toc-file %s*", operation, tocFile)
return fmt.Sprintf(`ps ux | grep "%s" | grep -v grep | awk '{print $2}'`, procPattern)
})
pidMap := make(map[string][]int)
for _, cmd := range remoteOutput.Commands {
pids := strings.Split(strings.TrimSpace(cmd.Stdout), "\n")
host := c.GetHostForContent(cmd.Content)
for _, pid := range pids {
if pid != "" {
foundPid = true
gplog.Debug("Found gpbackup_helper process %s for segment %d on host %s", pid, cmd.Content, host)
pidInt, _ := strconv.Atoi(pid)
pidMap[host] = append(pidMap[host], pidInt)
}
}
}
return pidMap, foundPid
}
// Checks for gpbackup_helper processes on segments and sends a USR1 signal to them to terminate.
// If the processes are not terminated within the timeout, a warning is logged.
// Ideally, the termination requests would only be sent to the segment hosts reported by CheckHelperPids,
// but the current design of cluster.GenerateAndExecuteCommand does not allow for that, so we a
// command to check for pids and send the termination signal to all segments if CheckHelperPids reports
// that there are still processes running.
func CleanUpSegmentHelperProcesses(c *cluster.Cluster, fpInfo filepath.FilePathInfo, operation string, timeout time.Duration) {
helperMutex.Lock()
defer helperMutex.Unlock()
tickerCleanup := time.NewTicker(1 * time.Second)
for {
helperPids, found := CheckHelperPids(c, fpInfo, operation)
select {
case <-tickerCleanup.C:
if !found {
return
}
c.GenerateAndExecuteCommand("Cleaning up gpbackup_helper processes", cluster.ON_SEGMENTS, func(contentID int) string {
tocFile := fpInfo.GetSegmentTOCFilePath(contentID)
procPattern := fmt.Sprintf("gpbackup_helper --%s-agent --toc-file %s", operation, tocFile)
return fmt.Sprintf("PIDS=`ps ux | grep \"%s\" | grep -v grep | awk '{print $2}'`; if [[ ! -z \"$PIDS\" ]]; then kill -USR1 $PIDS; fi", procPattern)
})
case <-time.After(timeout):
for host, pids := range helperPids {
if len(pids) > 0 {
gplog.Warn("Unable to terminate segment helper process(es) on host %s. ", host)
}
}
gplog.Warn("See %s for a complete list of segments with errors and terminate manually.", gplog.GetLogFilePath())
return
}
}
}
func CheckAgentErrorsOnSegments(c *cluster.Cluster, fpInfo filepath.FilePathInfo) error {
remoteOutput := c.GenerateAndExecuteCommand("Checking whether segment agents had errors", cluster.ON_SEGMENTS, func(contentID int) string {
errorFile := fmt.Sprintf("%s_error", fpInfo.GetSegmentPipeFilePath(contentID))
/*
* If an error file exists we want to indicate an error, as that means
* the agent errored out. If no file exists, the agent was successful.
*/
return fmt.Sprintf("if [[ -f %s ]]; then echo 'error'; fi; rm -f %s", errorFile, errorFile)
})
numErrors := 0
for contentID, cmd := range remoteOutput.Commands {
if strings.TrimSpace(cmd.Stdout) == "error" {
gplog.Verbose("Error occurred with helper agent on segment %d on host %s.", contentID, c.GetHostForContent(contentID))
numErrors++
}
}
if numErrors > 0 {
helperLogName := fpInfo.GetHelperLogPath()
return errors.Errorf("Encountered errors with %d helper agent(s). See %s for a complete list of segments with errors, and see %s on the corresponding hosts for detailed error messages.",
numErrors, gplog.GetLogFilePath(), helperLogName)
}
return nil
}
func CreateSkipFileOnSegments(oid string, tableName string, c *cluster.Cluster, fpInfo filepath.FilePathInfo) {
createSkipFileLogMsg := fmt.Sprintf("Creating skip file on segments for restore entry %s (%s)", oid, tableName)
remoteOutput := c.GenerateAndExecuteCommand(createSkipFileLogMsg, cluster.ON_SEGMENTS, func(contentID int) string {
return fmt.Sprintf("touch %s_skip_%s", fpInfo.GetSegmentPipeFilePath(contentID), oid)
})
c.CheckClusterError(remoteOutput, "Error while creating skip file on segments", func(contentID int) string {
return fmt.Sprintf("Could not create skip file %s_skip_%s on segments", fpInfo.GetSegmentPipeFilePath(contentID), oid)
})
}