vitess.io/vitess@v0.16.2/go/vt/mysqlctl/binlogs_gtid.go (about)

     1  /*
     2  Copyright 2022 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mysqlctl
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"vitess.io/vitess/go/mysql"
    26  	"vitess.io/vitess/go/vt/proto/vtrpc"
    27  	"vitess.io/vitess/go/vt/vterrors"
    28  )
    29  
    30  type BackupManifestPath []*BackupManifest
    31  
    32  func (p *BackupManifestPath) String() string {
    33  	var sb strings.Builder
    34  	sb.WriteString("BackupManifestPath: [")
    35  	for i, m := range *p {
    36  		if i > 0 {
    37  			sb.WriteString(", ")
    38  		}
    39  		if m.Incremental {
    40  			sb.WriteString("incremental:")
    41  		} else {
    42  			sb.WriteString("full:")
    43  		}
    44  		sb.WriteString(fmt.Sprintf("%v...%v", m.FromPosition, m.Position))
    45  	}
    46  	sb.WriteString("]")
    47  	return sb.String()
    48  }
    49  
    50  // ChooseBinlogsForIncrementalBackup chooses which binary logs need to be backed up in an incremental backup,
    51  // given a list of known binary logs, a function that returns the "Previous GTIDs" per binary log, and a
    52  // position from which to backup (normally the position of last known backup)
    53  // The function returns an error if the request could not be fulfilled: whether backup is not at all
    54  // possible, or is empty.
    55  func ChooseBinlogsForIncrementalBackup(
    56  	ctx context.Context,
    57  	lookFromGTIDSet mysql.GTIDSet,
    58  	binaryLogs []string,
    59  	pgtids func(ctx context.Context, binlog string) (gtids string, err error),
    60  	unionPreviousGTIDs bool,
    61  ) (
    62  	binaryLogsToBackup []string,
    63  	incrementalBackupFromGTID string,
    64  	incrementalBackupToGTID string,
    65  	err error,
    66  ) {
    67  
    68  	var prevGTIDsUnion mysql.GTIDSet
    69  	for i, binlog := range binaryLogs {
    70  		previousGtids, err := pgtids(ctx, binlog)
    71  		if err != nil {
    72  			return nil, "", "", vterrors.Wrapf(err, "cannot get previous gtids for binlog %v", binlog)
    73  		}
    74  		prevPos, err := mysql.ParsePosition(mysql.Mysql56FlavorID, previousGtids)
    75  		if err != nil {
    76  			return nil, "", "", vterrors.Wrapf(err, "cannot decode binlog %s position in incremental backup: %v", binlog, prevPos)
    77  		}
    78  		if prevGTIDsUnion == nil {
    79  			prevGTIDsUnion = prevPos.GTIDSet
    80  		} else {
    81  			prevGTIDsUnion = prevGTIDsUnion.Union(prevPos.GTIDSet)
    82  		}
    83  
    84  		containedInFromPos := lookFromGTIDSet.Contains(prevPos.GTIDSet)
    85  		// The binary logs are read in-order. They are build one on top of the other: we know
    86  		// the PreviousGTIDs of once binary log fully cover the previous binary log's.
    87  		if containedInFromPos {
    88  			// All previous binary logs are fully contained by backupPos. Carry on
    89  			continue
    90  		}
    91  		// We look for the first binary log whose "PreviousGTIDs" isn't already fully covered
    92  		// by "backupPos" (the position from which we want to create the inreemental backup).
    93  		// That means the *previous* binary log is the first binary log to introduce GTID events on top
    94  		// of "backupPos"
    95  		if i == 0 {
    96  			return nil, "", "", vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "the very first binlog file %v has PreviousGTIDs %s that exceed given incremental backup pos. There are GTID entries that are missing and this backup cannot run", binlog, prevPos)
    97  		}
    98  		if unionPreviousGTIDs {
    99  			prevPos.GTIDSet = prevGTIDsUnion
   100  		}
   101  		if !prevPos.GTIDSet.Contains(lookFromGTIDSet) {
   102  			return nil, "", "", vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "binary log %v with previous GTIDS %s neither contains requested GTID %s nor contains it. Backup cannot take place", binlog, prevPos.GTIDSet, lookFromGTIDSet)
   103  		}
   104  		// We begin with the previous binary log, and we ignore the last binary log, because it's still open and being written to.
   105  		binaryLogsToBackup = binaryLogs[i-1 : len(binaryLogs)-1]
   106  		incrementalBackupFromGTID, err := pgtids(ctx, binaryLogsToBackup[0])
   107  		if err != nil {
   108  			return nil, "", "", vterrors.Wrapf(err, "cannot evaluate incremental backup from pos")
   109  		}
   110  		// The "previous GTIDs" of the binary logs that _follows_ our binary-logs-to-backup indicates
   111  		// the backup's position.
   112  		incrementalBackupToGTID, err := pgtids(ctx, binaryLogs[len(binaryLogs)-1])
   113  		if err != nil {
   114  			return nil, "", "", vterrors.Wrapf(err, "cannot evaluate incremental backup to pos")
   115  		}
   116  		return binaryLogsToBackup, incrementalBackupFromGTID, incrementalBackupToGTID, nil
   117  	}
   118  	return nil, "", "", vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "no binary logs to backup (increment is empty)")
   119  }
   120  
   121  // IsValidIncrementalBakcup determines whether the given manifest can be used to extend a backup
   122  // based on baseGTIDSet. The manifest must be able to pick up from baseGTIDSet, and must extend it by at least
   123  // one entry.
   124  func IsValidIncrementalBakcup(baseGTIDSet mysql.GTIDSet, purgedGTIDSet mysql.GTIDSet, manifest *BackupManifest) bool {
   125  	if manifest == nil {
   126  		return false
   127  	}
   128  	if !manifest.Incremental {
   129  		return false
   130  	}
   131  	// We want to validate:
   132  	// manifest.FromPosition <= baseGTID < manifest.Position
   133  	if !baseGTIDSet.Contains(manifest.FromPosition.GTIDSet) {
   134  		// the incremental backup has a gap from the base set.
   135  		return false
   136  	}
   137  	if baseGTIDSet.Contains(manifest.Position.GTIDSet) {
   138  		// the incremental backup adds nothing; it's already contained in the base set
   139  		return false
   140  	}
   141  	if !manifest.Position.GTIDSet.Union(purgedGTIDSet).Contains(baseGTIDSet) {
   142  		// the base set seems to have extra entries?
   143  		return false
   144  	}
   145  	return true
   146  }
   147  
   148  // FindPITRPath evaluates the shortest path to recover a restoreToGTIDSet. The past is composed of:
   149  // - a full backup, followed by:
   150  // - zero or more incremental backups
   151  // The path ends with restoreToGTIDSet or goes beyond it. No shorter path will do the same.
   152  // The function returns an error when a path cannot be found.
   153  func FindPITRPath(restoreToGTIDSet mysql.GTIDSet, manifests [](*BackupManifest)) (shortestPath [](*BackupManifest), err error) {
   154  	sortedManifests := make([](*BackupManifest), 0, len(manifests))
   155  	for _, m := range manifests {
   156  		if m != nil {
   157  			sortedManifests = append(sortedManifests, m)
   158  		}
   159  	}
   160  	sort.SliceStable(sortedManifests, func(i, j int) bool {
   161  		return sortedManifests[j].Position.GTIDSet.Union(sortedManifests[i].PurgedPosition.GTIDSet).Contains(sortedManifests[i].Position.GTIDSet)
   162  	})
   163  	mostRelevantFullBackupIndex := -1 // an invalid value
   164  	for i, manifest := range sortedManifests {
   165  		if manifest.Incremental {
   166  			continue
   167  		}
   168  		if restoreToGTIDSet.Contains(manifest.Position.GTIDSet) {
   169  			// This backup is <= desired restore point, therefore it's valid
   170  			mostRelevantFullBackupIndex = i
   171  		}
   172  	}
   173  
   174  	if mostRelevantFullBackupIndex < 0 {
   175  		// No full backup prior to desired restore point...
   176  		return nil, vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "no full backup found before GTID %v", restoreToGTIDSet)
   177  	}
   178  	// All that interests us starts with mostRelevantFullBackupIndex: that's where the full backup is,
   179  	// and any relevant incremental backups follow that point (because manifests are sorted by backup pos, ascending)
   180  	sortedManifests = sortedManifests[mostRelevantFullBackupIndex:]
   181  	// Of all relevant backups, we take the most recent one.
   182  	fullBackup := sortedManifests[0]
   183  	if restoreToGTIDSet.Equal(fullBackup.Position.GTIDSet) {
   184  		// Perfect match, we don't need to look for incremental backups.
   185  		// We just skip the complexity of the followup section.
   186  		// The result path is a single full backup.
   187  		return append(shortestPath, fullBackup), nil
   188  	}
   189  	purgedGTIDSet := fullBackup.PurgedPosition.GTIDSet
   190  
   191  	var validRestorePaths []BackupManifestPath
   192  	// recursive function that searches for all possible paths:
   193  	var findPaths func(baseGTIDSet mysql.GTIDSet, pathManifests []*BackupManifest, remainingManifests []*BackupManifest)
   194  	findPaths = func(baseGTIDSet mysql.GTIDSet, pathManifests []*BackupManifest, remainingManifests []*BackupManifest) {
   195  		// The algorithm was first designed to find all possible paths. But then we recognized that it will be
   196  		// doing excessive work. At this time we choose to end the search once we find the first valid path, even if
   197  		// it's not the most optimal. The next "if" statement is the addition to the algorithm, where we suffice with
   198  		// a single result.
   199  		if len(validRestorePaths) > 0 {
   200  			return
   201  		}
   202  		// remove the above if you wish to explore all paths.
   203  		if baseGTIDSet.Contains(restoreToGTIDSet) {
   204  			// successful end of path. Update list of successful paths
   205  			validRestorePaths = append(validRestorePaths, pathManifests)
   206  			return
   207  		}
   208  		if len(remainingManifests) == 0 {
   209  			// end of the road. No possibilities from here.
   210  			return
   211  		}
   212  		// if the next manifest is eligible to be part of the path, try it out
   213  		if IsValidIncrementalBakcup(baseGTIDSet, purgedGTIDSet, remainingManifests[0]) {
   214  			nextGTIDSet := baseGTIDSet.Union(remainingManifests[0].Position.GTIDSet)
   215  			findPaths(nextGTIDSet, append(pathManifests, remainingManifests[0]), remainingManifests[1:])
   216  		}
   217  		// also, try without the next manifest
   218  		findPaths(baseGTIDSet, pathManifests, remainingManifests[1:])
   219  	}
   220  	// find all paths, entry point
   221  	findPaths(fullBackup.Position.GTIDSet, sortedManifests[0:1], sortedManifests[1:])
   222  	if len(validRestorePaths) == 0 {
   223  		return nil, vterrors.Errorf(vtrpc.Code_FAILED_PRECONDITION, "no path found that leads to GTID %v", restoreToGTIDSet)
   224  	}
   225  	// Now find a shortest path
   226  	for i := range validRestorePaths {
   227  		path := validRestorePaths[i]
   228  		if shortestPath == nil {
   229  			shortestPath = path
   230  			continue
   231  		}
   232  		if len(path) < len(shortestPath) {
   233  			shortestPath = path
   234  		}
   235  	}
   236  	return shortestPath, nil
   237  }