blob: 13ec421893e70465f5fa3fd709b50a70359000ee [file]
package restore
/*
* This file contains structs and functions related to backing up data on the segments.
*/
import (
"fmt"
"sync"
"sync/atomic"
"github.com/apache/cloudberry-backup/filepath"
"github.com/apache/cloudberry-backup/options"
"github.com/apache/cloudberry-backup/toc"
"github.com/apache/cloudberry-backup/utils"
"github.com/apache/cloudberry-go-libs/cluster"
"github.com/apache/cloudberry-go-libs/dbconn"
"github.com/apache/cloudberry-go-libs/gplog"
"github.com/jackc/pgx/v5/pgconn"
"github.com/pkg/errors"
"gopkg.in/cheggaaa/pb.v1"
)
var (
tableDelim = ","
)
func CopyTableIn(connectionPool *dbconn.DBConn, tableName string, tableAttributes string, destinationToRead string, singleDataFile bool, whichConn int) (int64, error) {
if wasTerminated {
return -1, nil
}
whichConn = connectionPool.ValidateConnNum(whichConn)
copyCommand := ""
readFromDestinationCommand := "cat"
customPipeThroughCommand := utils.GetPipeThroughProgram().InputCommand
resizeCluster := MustGetFlagBool(options.RESIZE_CLUSTER)
if singleDataFile || resizeCluster {
//helper.go handles compression, so we don't want to set it here
customPipeThroughCommand = utils.DefaultPipeThroughProgram
} else if MustGetFlagString(options.PLUGIN_CONFIG) != "" {
readFromDestinationCommand = fmt.Sprintf("%s restore_data %s", pluginConfig.ExecutablePath, pluginConfig.ConfigPath)
}
if customPipeThroughCommand == utils.DefaultPipeThroughProgram {
copyCommand = fmt.Sprintf("PROGRAM '%s %s'", readFromDestinationCommand, destinationToRead)
} else {
copyCommand = fmt.Sprintf("PROGRAM '%s %s | %s'", readFromDestinationCommand, destinationToRead, customPipeThroughCommand)
}
query := fmt.Sprintf("COPY %s%s FROM %s WITH CSV DELIMITER '%s' ON SEGMENT;", tableName, tableAttributes, copyCommand, tableDelim)
if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
utils.LogProgress(`Executing "%s" on coordinator`, query)
} else {
utils.LogProgress(`Executing "%s" on master`, query)
}
result, err := connectionPool.Exec(query, whichConn)
if err != nil {
errStr := fmt.Sprintf("Error loading data into table %s", tableName)
// The COPY ON SEGMENT error might contain useful CONTEXT output
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Where != "" {
errStr = fmt.Sprintf("%s: %s", errStr, pgErr.Where)
}
err = errors.Wrap(err, errStr)
return 0, err
}
rowsLoaded, _ := result.RowsAffected()
return rowsLoaded, nil
}
func restoreSingleTableData(fpInfo *filepath.FilePathInfo, entry toc.CoordinatorDataEntry, tableName string, whichConn int) error {
origSize, destSize, resizeCluster, batches := GetResizeClusterInfo()
var lastErr error
var numRowsRestored int64
// We don't want duplicate data for replicated tables so only do one batch
if entry.IsReplicated {
batches = 1
}
for i := 0; i < batches; i++ {
destinationToRead := ""
if backupConfig.SingleDataFile || resizeCluster {
destinationToRead = fmt.Sprintf("%s_%d_%d", fpInfo.GetSegmentPipePathForCopyCommand(), entry.Oid, i)
} else {
destinationToRead = fpInfo.GetTableBackupFilePathForCopyCommand(entry.Oid, utils.GetPipeThroughProgram().Extension, backupConfig.SingleDataFile)
}
gplog.Debug("Reading from %s", destinationToRead)
if entry.DistByEnum {
gplog.Verbose("Setting gp_enable_segment_copy_checking TO off for table %s", tableName)
connectionPool.MustExec("SET gp_enable_segment_copy_checking TO off;", whichConn)
defer connectionPool.MustExec("RESET gp_enable_segment_copy_checking;", whichConn)
}
// In the case where an error file is found, this means that the
// restore_helper has encountered an error and has shutdown.
// If this occurs we need to error out, as subsequent COPY statements
// will hang indefinitely waiting to read from pipes that the helper
// was expected to set up
if backupConfig.SingleDataFile {
agentErr := utils.CheckAgentErrorsOnSegments(globalCluster, globalFPInfo)
gplog.FatalOnError(agentErr)
}
partialRowsRestored, copyErr := CopyTableIn(connectionPool, tableName, entry.AttributeString, destinationToRead, backupConfig.SingleDataFile, whichConn)
if copyErr != nil {
gplog.Error("%s", copyErr.Error())
if MustGetFlagBool(options.ON_ERROR_CONTINUE) {
if ((connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB()) && backupConfig.SingleDataFile {
// inform segment helpers to skip this entry
utils.CreateSkipFileOnSegments(fmt.Sprintf("%d", entry.Oid), tableName, globalCluster, globalFPInfo)
}
lastErr = copyErr
copyErr = nil
continue
}
return copyErr
}
numRowsRestored += partialRowsRestored
}
if lastErr != nil {
return lastErr
}
numRowsBackedUp := entry.RowsCopied
// For replicated tables, we don't restore second and subsequent batches of data in the larger-to-smaller case,
// as that would duplicate data, so we have to "scale down" the values to determine whether the correct number
// of rows was restored
if entry.IsReplicated && origSize > destSize {
numRowsBackedUp /= int64(origSize)
numRowsRestored /= int64(destSize)
}
// TODO: When restoring a replicated table to a cluster with a different number of segments (resize),
// this check may fail for Cloudberry. This is because gprestore assumes that the COPY
// command for a replicated table returns the total number of rows copied across all segments
// (N * rows_per_segment) and divides the result by N to get the actual row count.
// However, due to Cloudberry issue https://github.com/apache/cloudberry/issues/1298,
// its COPY ON SEGMENT command returns only the base row count (rows_per_segment).
// The subsequent division in gprestore leads to a miscalculated row count, causing this check to fail.
err := CheckRowsRestored(numRowsRestored, numRowsBackedUp, tableName)
if err != nil {
gplog.Error("%s", err.Error())
return err
}
if resizeCluster || entry.DistByEnum {
// replicated tables cannot be redistributed, so instead expand them if needed
if entry.IsReplicated && (origSize < destSize) {
err := ExpandReplicatedTable(origSize, tableName, whichConn)
if err != nil {
gplog.Error("%s", err.Error())
}
} else {
err := RedistributeTableData(tableName, whichConn)
if err != nil {
gplog.Error("%s", err.Error())
}
}
}
return nil
}
func ExpandReplicatedTable(origSize int, tableName string, whichConn int) error {
// Replicated tables will only be initially restored to the segments backup was run from, and
// redistributing does not cause the data to be replicated to the new segments.
// To work around this, update the distribution policy entry for those tables to the original cluster size
// and then explicitly expand them to cause the data to be replicated to all new segments.
gplog.Debug("Distributing replicated data for %s", tableName)
alterDistPolQuery := fmt.Sprintf("UPDATE gp_distribution_policy SET numsegments=%d WHERE localoid = '%s'::regclass::oid", origSize, utils.EscapeSingleQuotes(tableName))
_, err := connectionPool.Exec(alterDistPolQuery, whichConn)
if err != nil {
return err
}
expandTableQuery := fmt.Sprintf("ALTER TABLE %s EXPAND TABLE;", tableName)
_, err = connectionPool.Exec(expandTableQuery, whichConn)
if err != nil {
return err
}
return nil
}
func CheckRowsRestored(rowsRestored int64, rowsBackedUp int64, tableName string) error {
if rowsRestored != rowsBackedUp {
rowsErrMsg := fmt.Sprintf("Expected to restore %d rows to table %s, but restored %d instead", rowsBackedUp, tableName, rowsRestored)
return errors.New(rowsErrMsg)
}
return nil
}
func RedistributeTableData(tableName string, whichConn int) error {
gplog.Debug("Redistributing data for %s", tableName)
query := fmt.Sprintf("ALTER TABLE %s SET WITH (REORGANIZE=true)", tableName)
_, err := connectionPool.Exec(query, whichConn)
return err
}
func restoreDataFromTimestamp(fpInfo filepath.FilePathInfo, dataEntries []toc.CoordinatorDataEntry,
gucStatements []toc.StatementWithType, dataProgressBar utils.ProgressBar) int32 {
totalTables := len(dataEntries)
if totalTables == 0 {
gplog.Verbose("No data to restore for timestamp = %s", fpInfo.Timestamp)
return 0
}
origSize, destSize, resizeCluster, batches := GetResizeClusterInfo()
if backupConfig.SingleDataFile || resizeCluster {
msg := ""
if backupConfig.SingleDataFile {
msg += "single data file "
}
if resizeCluster {
msg += "resize "
}
gplog.Verbose("Initializing pipes and gpbackup_helper on segments for %srestore", msg)
utils.VerifyHelperVersionOnSegments(version, globalCluster)
// During a larger-to-smaller restore, we need to do multiple passes of
// data loading so we assign the batches here.
oidList := make([]string, 0)
for _, entry := range dataEntries {
if entry.IsReplicated {
oidList = append(oidList, fmt.Sprintf("%d,0", entry.Oid))
continue
}
for b := 0; b < batches; b++ {
oidList = append(oidList, fmt.Sprintf("%d,%d", entry.Oid, b))
}
}
utils.WriteOidListToSegments(oidList, globalCluster, fpInfo, "oid")
initialPipes := CreateInitialSegmentPipes(oidList, globalCluster, connectionPool, fpInfo, batches)
if wasTerminated {
return 0
}
isFilter := false
if len(opts.IncludedRelations) > 0 || len(opts.ExcludedRelations) > 0 || len(opts.IncludedSchemas) > 0 || len(opts.ExcludedSchemas) > 0 {
isFilter = true
}
compressStr := ""
if backupConfig.Compressed {
compressStr = fmt.Sprintf(" --compression-type %s ", utils.GetPipeThroughProgram().Name)
}
utils.StartGpbackupHelpers(globalCluster, fpInfo, "--restore-agent", MustGetFlagString(options.PLUGIN_CONFIG), compressStr, MustGetFlagBool(options.ON_ERROR_CONTINUE), isFilter, &wasTerminated, initialPipes, backupConfig.SingleDataFile, resizeCluster, origSize, destSize, gplog.GetVerbosity())
}
/*
* We break when an interrupt is received and rely on
* TerminateHangingCopySessions to stop any COPY
* statements in progress if they don't finish on their own.
*/
var tableNum int64 = 0
tasks := make(chan toc.CoordinatorDataEntry, totalTables)
var workerPool sync.WaitGroup
var numErrors int32
var mutex = &sync.Mutex{}
panicChan := make(chan error)
for i := 0; i < connectionPool.NumConns; i++ {
workerPool.Add(1)
go func(whichConn int) {
defer func() {
if panicErr := recover(); panicErr != nil {
panicChan <- fmt.Errorf("%v", panicErr)
}
}()
defer workerPool.Done()
setGUCsForConnection(gucStatements, whichConn)
for entry := range tasks {
if wasTerminated {
dataProgressBar.(*pb.ProgressBar).NotPrint = true
return
}
tableName := utils.MakeFQN(entry.Schema, entry.Name)
if opts.RedirectSchema != "" {
tableName = utils.MakeFQN(opts.RedirectSchema, entry.Name)
}
// Truncate table before restore, if needed
var err error
if MustGetFlagBool(options.INCREMENTAL) || MustGetFlagBool(options.TRUNCATE_TABLE) {
gplog.Verbose("Truncating table %s prior to restoring data", tableName)
_, err = connectionPool.Exec(`TRUNCATE `+tableName, whichConn)
if err != nil {
gplog.Error("%s", err.Error())
}
}
if err == nil {
err = restoreSingleTableData(&fpInfo, entry, tableName, whichConn)
}
if err != nil {
atomic.AddInt32(&numErrors, 1)
if !MustGetFlagBool(options.ON_ERROR_CONTINUE) {
dataProgressBar.(*pb.ProgressBar).NotPrint = true
return
}
mutex.Lock()
errorTablesData[tableName] = Empty{}
mutex.Unlock()
} else {
utils.LogProgress("Restored data to table %s from file (table %d of %d)", tableName, atomic.AddInt64(&tableNum, 1), totalTables)
}
dataProgressBar.Increment()
}
}(i)
}
for _, entry := range dataEntries {
tasks <- entry
}
close(tasks)
workerPool.Wait()
// Allow panics to crash from the main process, invoking DoCleanup
select {
case err := <-panicChan:
gplog.Fatal(err, "")
default:
// no panic, nothing to do
}
if numErrors > 0 {
fmt.Println("")
gplog.Error("Encountered %d error(s) during table data restore; see log file %s for a list of table errors.", numErrors, gplog.GetLogFilePath())
}
return numErrors
}
func CreateInitialSegmentPipes(oidList []string, c *cluster.Cluster, connectionPool *dbconn.DBConn, fpInfo filepath.FilePathInfo, batches int) int {
// oidList is laid out in oid-major order: [T1B0, T1B1, ..., T2B0, T2B1, ...].
// Workers dispatch one task per table and iterate batches sequentially within
// restoreSingleTableData, so NumConns concurrent workers may request pipes at
// oidList indices 0, batches, 2*batches, ..., (NumConns-1)*batches before the
// helper has had a chance to create any of them. Preload NumConns*batches
// pipes so every concurrent worker's first batch is covered; the helper then
// rolls the queue forward as each batch completes.
maxPipes := connectionPool.NumConns * batches
if maxPipes > len(oidList) {
maxPipes = len(oidList)
}
for i := 0; i < maxPipes; i++ {
utils.CreateSegmentPipeOnAllHostsForRestore(oidList[i], c, fpInfo)
}
return maxPipes
}