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 }