| package backup |
| |
| /* |
| * This file contains structs and functions related to backing up data on the segments. |
| */ |
| |
| import ( |
| "errors" |
| "fmt" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "time" |
| |
| "github.com/apache/cloudberry-backup/options" |
| "github.com/apache/cloudberry-backup/utils" |
| "github.com/apache/cloudberry-go-libs/dbconn" |
| "github.com/apache/cloudberry-go-libs/gplog" |
| "github.com/jackc/pgconn" |
| "gopkg.in/cheggaaa/pb.v1" |
| ) |
| |
| var ( |
| tableDelim = "," |
| ) |
| |
| func ConstructTableAttributesList(columnDefs []ColumnDefinition) string { |
| // this attribute list used ONLY by CopyTableIn on the restore side |
| // columns where data should not be copied out and back in are excluded from this string. |
| names := make([]string, 0) |
| for _, col := range columnDefs { |
| // data in generated columns should not be backed up or restored. |
| if col.AttGenerated == "" { |
| names = append(names, col.Name) |
| } |
| } |
| if len(names) > 0 { |
| return fmt.Sprintf("(%s)", strings.Join(names, ",")) |
| } |
| return "" |
| } |
| |
| func AddTableDataEntriesToTOC(tables []Table, rowsCopiedMaps []map[uint32]int64) { |
| for _, table := range tables { |
| if !table.SkipDataBackup() { |
| var rowsCopied int64 |
| for _, rowsCopiedMap := range rowsCopiedMaps { |
| if val, ok := rowsCopiedMap[table.Oid]; ok { |
| rowsCopied = val |
| break |
| } |
| } |
| attributes := ConstructTableAttributesList(table.ColumnDefs) |
| globalTOC.AddCoordinatorDataEntry(table.Schema, table.Name, table.Oid, attributes, rowsCopied, table.PartitionLevelInfo.RootName, table.DistPolicy.Policy, table.DistPolicy.DistByEnum) |
| } |
| } |
| } |
| |
| type BackupProgressCounters struct { |
| NumRegTables int64 |
| TotalRegTables int64 |
| ProgressBar utils.ProgressBar |
| } |
| |
| func CopyTableOut(connectionPool *dbconn.DBConn, table Table, destinationToWrite string, connNum int) (int64, error) { |
| if wasTerminated { |
| return -1, nil |
| } |
| checkPipeExistsCommand := "" |
| customPipeThroughCommand := utils.GetPipeThroughProgram().OutputCommand |
| sendToDestinationCommand := ">" |
| if MustGetFlagBool(options.SINGLE_DATA_FILE) { |
| /* |
| * The segment TOC files are always written to the segment data directory for |
| * performance reasons, in case the user-specified directory is on a mounted |
| * drive. It will be copied to a user-specified directory, if any, once all |
| * of the data is backed up. |
| */ |
| checkPipeExistsCommand = fmt.Sprintf("(test -p \"%s\" || (echo \"Pipe not found %s\">&2; exit 1)) && ", destinationToWrite, destinationToWrite) |
| customPipeThroughCommand = utils.DefaultPipeThroughProgram |
| } else if MustGetFlagString(options.PLUGIN_CONFIG) != "" { |
| sendToDestinationCommand = fmt.Sprintf("| %s backup_data %s", pluginConfig.ExecutablePath, pluginConfig.ConfigPath) |
| } |
| |
| copyCommand := fmt.Sprintf("PROGRAM '%s%s %s %s'", checkPipeExistsCommand, customPipeThroughCommand, sendToDestinationCommand, destinationToWrite) |
| |
| columnNames := "" |
| if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { |
| // process column names to exclude generated columns from data copy out |
| columnNames = ConstructTableAttributesList(table.ColumnDefs) |
| } |
| |
| workerInfo := "" |
| if gplog.GetVerbosity() >= gplog.LOGVERBOSE { |
| workerInfo = fmt.Sprintf("Worker %d: ", connNum) |
| } |
| query := fmt.Sprintf("COPY %s%s TO %s WITH CSV DELIMITER '%s' ON SEGMENT IGNORE EXTERNAL PARTITIONS;", table.FQN(), columnNames, copyCommand, tableDelim) |
| if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() { |
| utils.LogProgress(`%sExecuting "%s" on coordinator`, workerInfo, query) |
| } else { |
| utils.LogProgress(`%sExecuting "%s" on master`, workerInfo, query) |
| } |
| result, err := connectionPool.Exec(query, connNum) |
| if err != nil { |
| return 0, err |
| } |
| numRows, _ := result.RowsAffected() |
| |
| return numRows, nil |
| } |
| |
| func BackupSingleTableData(table Table, rowsCopiedMap map[uint32]int64, counters *BackupProgressCounters, whichConn int) error { |
| workerInfo := "" |
| if gplog.GetVerbosity() >= gplog.LOGVERBOSE { |
| workerInfo = fmt.Sprintf("Worker %d: ", whichConn) |
| } |
| logMessage := fmt.Sprintf("%sWriting data for table %s to file", workerInfo, table.FQN()) |
| // Avoid race condition by incrementing counters in call to sprintf |
| tableCount := fmt.Sprintf(" (table %d of %d)", atomic.AddInt64(&counters.NumRegTables, 1), counters.TotalRegTables) |
| utils.LogProgress(logMessage + tableCount) |
| |
| destinationToWrite := "" |
| if MustGetFlagBool(options.SINGLE_DATA_FILE) { |
| destinationToWrite = fmt.Sprintf("%s_%d", globalFPInfo.GetSegmentPipePathForCopyCommand(), table.Oid) |
| } else { |
| destinationToWrite = globalFPInfo.GetTableBackupFilePathForCopyCommand(table.Oid, utils.GetPipeThroughProgram().Extension, false) |
| } |
| rowsCopied, err := CopyTableOut(connectionPool, table, destinationToWrite, whichConn) |
| if err != nil { |
| return err |
| } |
| rowsCopiedMap[table.Oid] = rowsCopied |
| counters.ProgressBar.Increment() |
| return nil |
| } |
| |
| /* |
| * Iterate through tables, backup data from each table in set. |
| * If supported by the database, a synchronized database snapshot is used to sync |
| * all parallel workers' view of the database and ensure data consistency across workers. |
| * In each worker, we try to acquire a ACCESS SHARE lock on each table in NOWAIT mode, |
| * to avoid potential deadlocks with queued DDL operations that requested ACCESS EXCLUSIVE |
| * locks on the same tables (for eg concurrent ALTER TABLE operations). If worker is unable to |
| * acquire a lock, defer table to worker 0, which holds all ACCESS SHARE locks for the backup set. |
| * If synchronized snapshot is not supported and worker is unable to acquire a lock, the |
| * worker must be terminated because the session no longer has a valid distributed snapshot |
| * |
| * FIXME: Simplify BackupDataForAllTables by having one function for snapshot workflow and |
| * another without, then extract common portions into their own functions. |
| */ |
| func BackupDataForAllTables(tables []Table) []map[uint32]int64 { |
| counters := BackupProgressCounters{NumRegTables: 0, TotalRegTables: int64(len(tables))} |
| counters.ProgressBar = utils.NewProgressBar(int(counters.TotalRegTables), "Tables backed up: ", utils.PB_INFO) |
| counters.ProgressBar.Start() |
| rowsCopiedMaps := make([]map[uint32]int64, connectionPool.NumConns) |
| /* |
| * We break when an interrupt is received and rely on |
| * TerminateHangingCopySessions to halt any COPY statements |
| * in progress if they don't finish on their own. |
| */ |
| tasks := make(chan Table, len(tables)) |
| var oidMap sync.Map |
| var isErroredBackup atomic.Bool |
| var workerPool sync.WaitGroup |
| // Record and track tables in a hashmap of oids and table states (preloaded with value Unknown). |
| // The tables are loaded into the tasks channel for the subsequent goroutines to work on. |
| for _, table := range tables { |
| oidMap.Store(table.Oid, Unknown) |
| tasks <- table |
| } |
| |
| /* |
| * Worker 0 is a special database connection that |
| * 1) Exports the database snapshot if the feature is supported |
| * 2) Does not have tables pre-assigned to it. |
| * 3) Processes tables only in the event that the other workers encounter locking issues. |
| * Worker 0 already has all locks on the tables so it will not run into locking issues. |
| */ |
| panicChan := make(chan error) |
| rowsCopiedMaps[0] = make(map[uint32]int64) |
| for connNum := 1; connNum < connectionPool.NumConns; connNum++ { |
| rowsCopiedMaps[connNum] = make(map[uint32]int64) |
| workerPool.Add(1) |
| go func(whichConn int) { |
| defer func() { |
| if panicErr := recover(); panicErr != nil { |
| panicChan <- fmt.Errorf("%v", panicErr) |
| } |
| }() |
| defer workerPool.Done() |
| /* If the --leaf-partition-data flag is not set, the parent and all leaf |
| * partition data are treated as a single table and will be assigned to a single worker. |
| * Large partition hierarchies result in a large number of locks being held until the |
| * transaction commits and the locks are released. |
| */ |
| for table := range tasks { |
| if wasTerminated || isErroredBackup.Load() { |
| counters.ProgressBar.(*pb.ProgressBar).NotPrint = true |
| return |
| } |
| if backupSnapshot != "" && connectionPool.Tx[whichConn] == nil { |
| err := SetSynchronizedSnapshot(connectionPool, whichConn, backupSnapshot) |
| gplog.FatalOnError(err) |
| } |
| // If a random external SQL command had queued an AccessExclusiveLock acquisition |
| // request against this next table, the --job worker thread would deadlock on the |
| // COPY attempt. To prevent gpbackup from hanging, we attempt to acquire an |
| // AccessShareLock on the relation with the NOWAIT option before we run COPY. If |
| // the LOCK TABLE NOWAIT call fails, we catch the error and defer the table to the |
| // main worker thread, worker 0. Afterwards, if we are in a local transaction |
| // instead of a distributed snapshot, we break early and terminate the worker since |
| // its transaction is now in an aborted state. We do not need to do this with the |
| // main worker thread because it has already acquired AccessShareLocks on all |
| // tables before the metadata dumping part. |
| err := LockTableNoWait(table, whichConn) |
| if err != nil { |
| if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code != PG_LOCK_NOT_AVAILABLE { |
| isErroredBackup.Store(true) |
| err = connectionPool.Rollback(whichConn) |
| if err != nil { |
| gplog.Warn("Worker %d: %s", whichConn, err) |
| } |
| gplog.Fatal(fmt.Errorf("Unexpectedly unable to take lock on table %s, %s", table.FQN(), pgErr.Error()), "") |
| } |
| if gplog.GetVerbosity() < gplog.LOGVERBOSE { |
| // Add a newline to interrupt the progress bar so that |
| // the following WARN message is nicely outputted. |
| fmt.Printf("\n") |
| } |
| gplog.Warn("Worker %d could not acquire AccessShareLock for table %s.", whichConn, table.FQN()) |
| logTableLocks(table, whichConn) |
| // rollback transaction and defer table |
| err = connectionPool.Rollback(whichConn) |
| if err != nil { |
| gplog.Warn("Worker %d: %s", whichConn, err) |
| } |
| oidMap.Store(table.Oid, Deferred) |
| // If have backup snapshot, continue to next table, else terminate worker. |
| if backupSnapshot != "" { |
| continue |
| } else { |
| gplog.Warn("Terminating worker %d and deferring table %s to main worker thread.", whichConn, table.FQN()) |
| break |
| } |
| } |
| err = BackupSingleTableData(table, rowsCopiedMaps[whichConn], &counters, whichConn) |
| if err != nil { |
| // if copy isn't working, skip remaining backups, and let downstream panic |
| // handling deal with it |
| counters.ProgressBar.(*pb.ProgressBar).NotPrint = true |
| isErroredBackup.Store(true) |
| gplog.Fatal(err, "") |
| } else { |
| oidMap.Store(table.Oid, Complete) |
| } |
| if backupSnapshot != "" { |
| err = connectionPool.Commit(whichConn) |
| if err != nil { |
| gplog.Warn("Worker %d: %s", whichConn, err) |
| } |
| } |
| } |
| }(connNum) |
| } |
| |
| // Special goroutine to handle deferred tables |
| // Handle all tables deferred by the deadlock detection. This can only be |
| // done with the main worker thread, worker 0, because it has |
| // AccessShareLocks on all the tables already. |
| deferredWorkerDone := make(chan bool) |
| go func() { |
| defer func() { |
| if panicErr := recover(); panicErr != nil { |
| panicChan <- fmt.Errorf("%v", panicErr) |
| } |
| }() |
| for _, table := range tables { |
| for { |
| if wasTerminated || isErroredBackup.Load() { |
| return |
| } |
| state, _ := oidMap.Load(table.Oid) |
| if state.(int) == Unknown { |
| time.Sleep(time.Millisecond * 50) |
| } else if state.(int) == Deferred { |
| err := BackupSingleTableData(table, rowsCopiedMaps[0], &counters, 0) |
| if err != nil { |
| isErroredBackup.Store(true) |
| gplog.Fatal(err, "") |
| } |
| oidMap.Store(table.Oid, Complete) |
| break |
| } else if state.(int) == Complete { |
| break |
| } else { |
| gplog.Fatal(errors.New("Encountered unknown table state"), "") |
| } |
| } |
| } |
| deferredWorkerDone <- true |
| }() |
| |
| 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 not using synchronized snapshots, |
| // check if all workers were terminated due to lock issues. |
| if backupSnapshot == "" { |
| allWorkersTerminatedLogged := false |
| for _, table := range tables { |
| if wasTerminated || isErroredBackup.Load() { |
| counters.ProgressBar.(*pb.ProgressBar).NotPrint = true |
| break |
| } |
| state, _ := oidMap.Load(table.Oid) |
| if state == Unknown { |
| if !allWorkersTerminatedLogged { |
| gplog.Warn("All workers terminated due to lock issues. Falling back to single main worker.") |
| allWorkersTerminatedLogged = true |
| } |
| oidMap.Store(table.Oid, Deferred) |
| } |
| } |
| } |
| |
| // Main goroutine waits for deferred worker 0 by waiting on this channel |
| <-deferredWorkerDone |
| agentErr := utils.CheckAgentErrorsOnSegments(globalCluster, globalFPInfo) |
| if agentErr != nil { |
| gplog.Fatal(agentErr, "") |
| } |
| |
| counters.ProgressBar.Finish() |
| return rowsCopiedMaps |
| } |
| |
| func printDataBackupWarnings(numExtTables int64) { |
| if numExtTables > 0 { |
| gplog.Info("Skipped data backup of %d external/foreign table(s).", numExtTables) |
| gplog.Info("See %s for a complete list of skipped tables.", gplog.GetLogFilePath()) |
| } |
| } |
| |
| // Remove external/foreign tables from the data backup set |
| func GetBackupDataSet(tables []Table) ([]Table, int64) { |
| var backupDataSet []Table |
| var numExtOrForeignTables int64 |
| |
| if !backupReport.MetadataOnly { |
| for _, table := range tables { |
| if !table.SkipDataBackup() { |
| backupDataSet = append(backupDataSet, table) |
| } else { |
| gplog.Verbose("Skipping data backup of table %s because it is either an external or foreign table.", table.FQN()) |
| numExtOrForeignTables++ |
| } |
| } |
| } |
| return backupDataSet, numExtOrForeignTables |
| } |
| |
| // Acquire AccessShareLock on a table with NOWAIT option. If we are unable to acquire |
| // the lock, the call will fail instead of block. Return the failure for handling. |
| func LockTableNoWait(dataTable Table, connNum int) error { |
| var lockMode string |
| // TODO: This is a temporary workaround. Cloudberry should support the |
| // COORDINATOR ONLY syntax to align with GPDB 7+ for performance optimization |
| // during the table locking phase. Revert this to the combined GPDB 7+ condition |
| // once Cloudberry adds this feature. |
| if connectionPool.Version.IsCBDB() { |
| lockMode = `IN ACCESS SHARE MODE NOWAIT` |
| } else if connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7") { |
| lockMode = `IN ACCESS SHARE MODE NOWAIT COORDINATOR ONLY` |
| } else if connectionPool.Version.AtLeast("6.21.0") { |
| lockMode = `IN ACCESS SHARE MODE NOWAIT MASTER ONLY` |
| } else { |
| lockMode = `IN ACCESS SHARE MODE NOWAIT` |
| } |
| query := fmt.Sprintf("LOCK TABLE %s %s;", dataTable.FQN(), lockMode) |
| gplog.Verbose("Worker %d: %s", connNum, query) |
| _, err := connectionPool.Exec(query, connNum) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |