001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.backup.impl; 019 020import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS; 021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR; 022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.DEFAULT_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS; 023import static org.apache.hadoop.hbase.mapreduce.HFileOutputFormat2.MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY; 024import static org.apache.hadoop.hbase.mapreduce.WALPlayer.IGNORE_EMPTY_FILES; 025 026import java.io.IOException; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.Map; 031import java.util.concurrent.TimeUnit; 032import java.util.stream.Collectors; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.hadoop.fs.Path; 035import org.apache.hadoop.hbase.HBaseConfiguration; 036import org.apache.hadoop.hbase.TableName; 037import org.apache.hadoop.hbase.backup.BackupRestoreFactory; 038import org.apache.hadoop.hbase.backup.PointInTimeRestoreRequest; 039import org.apache.hadoop.hbase.backup.RestoreJob; 040import org.apache.hadoop.hbase.backup.RestoreRequest; 041import org.apache.hadoop.hbase.backup.util.BackupUtils; 042import org.apache.hadoop.hbase.client.Connection; 043import org.apache.hadoop.hbase.mapreduce.WALInputFormat; 044import org.apache.hadoop.hbase.mapreduce.WALPlayer; 045import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 046import org.apache.hadoop.util.Tool; 047import org.apache.yetus.audience.InterfaceAudience; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051/** 052 * Abstract base class for handling Point-In-Time Restore (PITR). 053 * <p> 054 * Defines the common PITR algorithm using the Template Method Pattern. Subclasses provide the 055 * metadata source (e.g., backup system table or a custom backup location). 056 * <p> 057 * The PITR flow includes: 058 * <ul> 059 * <li>Validating recovery time within the PITR window</li> 060 * <li>Checking for continuous backup and valid backup availability</li> 061 * <li>Restoring the backup</li> 062 * <li>Replaying WALs to bring tables to the target state</li> 063 * </ul> 064 * <p> 065 * Subclasses must implement {@link #getBackupMetadata(PointInTimeRestoreRequest)} to supply the 066 * list of completed backups. 067 */ 068@InterfaceAudience.Private 069public abstract class AbstractPitrRestoreHandler { 070 private static final Logger LOG = LoggerFactory.getLogger(AbstractPitrRestoreHandler.class); 071 072 protected final Connection conn; 073 protected final BackupAdminImpl backupAdmin; 074 protected final PointInTimeRestoreRequest request; 075 076 AbstractPitrRestoreHandler(Connection conn, PointInTimeRestoreRequest request) { 077 this.conn = conn; 078 this.backupAdmin = new BackupAdminImpl(conn); 079 this.request = request; 080 } 081 082 /** 083 * Validates the PITR request and performs the restore if valid. This is the main entry point for 084 * the PITR process and should be called by clients. 085 */ 086 public final void validateAndRestore() throws IOException { 087 long endTime = request.getToDateTime(); 088 validateRequestToTime(endTime); 089 090 TableName[] sourceTableArray = request.getFromTables(); 091 TableName[] targetTableArray = resolveTargetTables(sourceTableArray, request.getToTables()); 092 093 // Validate PITR requirements 094 validatePitr(endTime, sourceTableArray, targetTableArray); 095 096 // If only validation is required, log and return 097 if (request.isCheck()) { 098 LOG.info("PITR can be successfully executed"); 099 return; 100 } 101 102 // Execute PITR process 103 try (BackupSystemTable table = new BackupSystemTable(conn)) { 104 Map<TableName, Long> continuousBackupTables = table.getContinuousBackupTableSet(); 105 List<PitrBackupMetadata> backupMetadataList = getBackupMetadata(request); 106 107 for (int i = 0; i < sourceTableArray.length; i++) { 108 restoreTableWithWalReplay(sourceTableArray[i], targetTableArray[i], endTime, 109 continuousBackupTables, backupMetadataList, request); 110 } 111 } 112 } 113 114 /** 115 * Validates whether the requested end time falls within the allowed PITR recovery window. 116 * @param endTime The target recovery time. 117 * @throws IOException If the requested recovery time is outside the allowed window. 118 */ 119 private void validateRequestToTime(long endTime) throws IOException { 120 long pitrWindowDays = conn.getConfiguration().getLong(CONF_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS, 121 DEFAULT_CONTINUOUS_BACKUP_PITR_WINDOW_DAYS); 122 long currentTime = EnvironmentEdgeManager.getDelegate().currentTime(); 123 long pitrMaxStartTime = currentTime - TimeUnit.DAYS.toMillis(pitrWindowDays); 124 125 if (endTime < pitrMaxStartTime) { 126 String errorMsg = String.format( 127 "Requested recovery time (%d) is out of the allowed PITR window (last %d days).", endTime, 128 pitrWindowDays); 129 LOG.error(errorMsg); 130 throw new IOException(errorMsg); 131 } 132 133 if (endTime > currentTime) { 134 String errorMsg = String.format( 135 "Requested recovery time (%d) is in the future. Current time: %d.", endTime, currentTime); 136 LOG.error(errorMsg); 137 throw new IOException(errorMsg); 138 } 139 } 140 141 /** 142 * Resolves the target table array. If null or empty, defaults to the source table array. 143 */ 144 private TableName[] resolveTargetTables(TableName[] sourceTables, TableName[] targetTables) { 145 return (targetTables == null || targetTables.length == 0) ? sourceTables : targetTables; 146 } 147 148 /** 149 * Validates whether Point-In-Time Recovery (PITR) is possible for the given tables at the 150 * specified time. 151 * <p> 152 * PITR requires: 153 * <ul> 154 * <li>Continuous backup to be enabled for the source tables.</li> 155 * <li>A valid backup image and corresponding WALs to be available.</li> 156 * </ul> 157 * @param endTime The target recovery time. 158 * @param sTableArray The source tables to restore. 159 * @param tTableArray The target tables where the restore will be performed. 160 * @throws IOException If PITR is not possible due to missing continuous backup or backup images. 161 */ 162 private void validatePitr(long endTime, TableName[] sTableArray, TableName[] tTableArray) 163 throws IOException { 164 try (BackupSystemTable table = new BackupSystemTable(conn)) { 165 // Retrieve the set of tables with continuous backup enabled 166 Map<TableName, Long> continuousBackupTables = table.getContinuousBackupTableSet(); 167 168 // Ensure all source tables have continuous backup enabled 169 validateContinuousBackup(sTableArray, continuousBackupTables); 170 171 // Fetch completed backup information 172 List<PitrBackupMetadata> backupMetadataList = getBackupMetadata(request); 173 174 // Ensure a valid backup and WALs exist for PITR 175 validateBackupAvailability(sTableArray, tTableArray, endTime, continuousBackupTables, 176 backupMetadataList); 177 } 178 } 179 180 /** 181 * Ensures that all source tables have continuous backup enabled. 182 */ 183 private void validateContinuousBackup(TableName[] tables, 184 Map<TableName, Long> continuousBackupTables) throws IOException { 185 List<TableName> missingTables = 186 Arrays.stream(tables).filter(table -> !continuousBackupTables.containsKey(table)).toList(); 187 188 if (!missingTables.isEmpty()) { 189 String errorMsg = "Continuous Backup is not enabled for the following tables: " 190 + missingTables.stream().map(TableName::getNameAsString).collect(Collectors.joining(", ")); 191 LOG.error(errorMsg); 192 throw new IOException(errorMsg); 193 } 194 } 195 196 /** 197 * Ensures that a valid backup and corresponding WALs exist for PITR for each source table. PITR 198 * requires: 1. A valid backup available before the end time. 2. Write-Ahead Logs (WALs) covering 199 * the remaining duration up to the end time. 200 */ 201 private void validateBackupAvailability(TableName[] sTableArray, TableName[] tTableArray, 202 long endTime, Map<TableName, Long> continuousBackupTables, List<PitrBackupMetadata> backups) 203 throws IOException { 204 for (int i = 0; i < sTableArray.length; i++) { 205 if ( 206 !canPerformPitr(sTableArray[i], tTableArray[i], endTime, continuousBackupTables, backups) 207 ) { 208 String errorMsg = String.format( 209 "PITR failed: No valid backup/WALs found for source table %s (target: %s) before time %d", 210 sTableArray[i].getNameAsString(), tTableArray[i].getNameAsString(), endTime); 211 LOG.error(errorMsg); 212 throw new IOException(errorMsg); 213 } 214 } 215 } 216 217 /** 218 * Checks whether PITR can be performed for a given source-target table pair. 219 */ 220 private boolean canPerformPitr(TableName stableName, TableName tTableName, long endTime, 221 Map<TableName, Long> continuousBackupTables, List<PitrBackupMetadata> backups) { 222 return getValidBackup(stableName, tTableName, endTime, continuousBackupTables, backups) != null; 223 } 224 225 /** 226 * Finds and returns the first valid backup metadata entry that can be used to restore the given 227 * source table up to the specified end time. A backup is considered valid if: 228 * <ul> 229 * <li>It contains the source table</li> 230 * <li>It was completed before the requested end time</li> 231 * <li>Its start time is after the table's continuous backup start time</li> 232 * <li>It passes the restore request validation</li> 233 * </ul> 234 */ 235 private PitrBackupMetadata getValidBackup(TableName sTableName, TableName tTablename, 236 long endTime, Map<TableName, Long> continuousBackupTables, List<PitrBackupMetadata> backups) { 237 for (PitrBackupMetadata backup : backups) { 238 if (isValidBackupForPitr(backup, sTableName, endTime, continuousBackupTables)) { 239 240 RestoreRequest restoreRequest = 241 BackupUtils.createRestoreRequest(backup.getRootDir(), backup.getBackupId(), true, 242 new TableName[] { sTableName }, new TableName[] { tTablename }, false); 243 244 try { 245 if (backupAdmin.validateRequest(restoreRequest)) { 246 return backup; 247 } 248 } catch (IOException e) { 249 LOG.warn("Exception occurred while testing the backup : {} for restore ", 250 backup.getBackupId(), e); 251 } 252 } 253 } 254 return null; 255 } 256 257 /** 258 * Determines if the given backup is valid for PITR. 259 * <p> 260 * A backup is valid if: 261 * <ul> 262 * <li>It contains the source table.</li> 263 * <li>It was completed before the end time.</li> 264 * <li>The start timestamp of the backup is after the continuous backup start time for the 265 * table.</li> 266 * </ul> 267 * @param backupMetadata Backup information object. 268 * @param tableName Table to check. 269 * @param endTime The target recovery time. 270 * @param continuousBackupTables Map of tables with continuous backup enabled. 271 * @return true if the backup is valid for PITR, false otherwise. 272 */ 273 private boolean isValidBackupForPitr(PitrBackupMetadata backupMetadata, TableName tableName, 274 long endTime, Map<TableName, Long> continuousBackupTables) { 275 return backupMetadata.getTableNames().contains(tableName) 276 && backupMetadata.getCompleteTs() <= endTime 277 && continuousBackupTables.getOrDefault(tableName, 0L) <= backupMetadata.getStartTs(); 278 } 279 280 /** 281 * Restores the table using the selected backup and replays WALs from the backup start time to the 282 * requested end time. 283 * @throws IOException if no valid backup is found or WAL replay fails 284 */ 285 private void restoreTableWithWalReplay(TableName sourceTable, TableName targetTable, long endTime, 286 Map<TableName, Long> continuousBackupTables, List<PitrBackupMetadata> backupMetadataList, 287 PointInTimeRestoreRequest request) throws IOException { 288 PitrBackupMetadata backupMetadata = 289 getValidBackup(sourceTable, targetTable, endTime, continuousBackupTables, backupMetadataList); 290 if (backupMetadata == null) { 291 String errorMsg = "Could not find a valid backup and WALs for PITR for table: " 292 + sourceTable.getNameAsString(); 293 LOG.error(errorMsg); 294 throw new IOException(errorMsg); 295 } 296 297 RestoreRequest restoreRequest = BackupUtils.createRestoreRequest(backupMetadata.getRootDir(), 298 backupMetadata.getBackupId(), false, new TableName[] { sourceTable }, 299 new TableName[] { targetTable }, request.isOverwrite()); 300 301 backupAdmin.restore(restoreRequest); 302 replayWal(sourceTable, targetTable, backupMetadata.getStartTs(), endTime); 303 304 reBulkloadFiles(sourceTable, targetTable, backupMetadata.getStartTs(), endTime, 305 request.isKeepOriginalSplits(), request.getRestoreRootDir()); 306 } 307 308 /** 309 * Re-applies/re-bulkloads store files discovered from WALs into the target table. 310 * <p> 311 * <b>Note:</b> this method re-uses the same {@link RestoreJob} MapReduce job that we originally 312 * implemented for performing full and incremental backup restores. The MR job (obtained via 313 * {@link BackupRestoreFactory#getRestoreJob(Configuration)}) is used here to perform an HFile 314 * bulk-load of the discovered store files into {@code targetTable}. 315 * @param sourceTable source table name (used for locating bulk files and logging) 316 * @param targetTable destination table to bulk-load the HFiles into 317 * @param startTime start of WAL range (ms) 318 * @param endTime end of WAL range (ms) 319 * @param keepOriginalSplits pass-through flag to control whether original region splits are 320 * preserved 321 * @param restoreRootDir local/DFS path under which temporary and output dirs are created 322 * @throws IOException on IO or job failure 323 */ 324 private void reBulkloadFiles(TableName sourceTable, TableName targetTable, long startTime, 325 long endTime, boolean keepOriginalSplits, String restoreRootDir) throws IOException { 326 327 Configuration conf = HBaseConfiguration.create(conn.getConfiguration()); 328 conf.setBoolean(RestoreJob.KEEP_ORIGINAL_SPLITS_KEY, keepOriginalSplits); 329 330 String walBackupDir = conn.getConfiguration().get(CONF_CONTINUOUS_BACKUP_WAL_DIR); 331 Path walDirPath = new Path(walBackupDir); 332 conf.set(RestoreJob.BACKUP_ROOT_PATH_KEY, walDirPath.toString()); 333 334 RestoreJob restoreService = BackupRestoreFactory.getRestoreJob(conf); 335 336 List<Path> bulkloadFiles = BackupUtils.collectBulkFiles(conn, sourceTable, targetTable, 337 startTime, endTime, new Path(restoreRootDir), new ArrayList<String>()); 338 339 if (bulkloadFiles.isEmpty()) { 340 LOG.info("No bulk-load files found for {} in time range {}-{}. Skipping bulkload restore.", 341 sourceTable, startTime, endTime); 342 return; 343 } 344 345 Path[] pathsArray = bulkloadFiles.toArray(new Path[0]); 346 347 try { 348 // Use the existing RestoreJob MR job (the same MapReduce job used for full/incremental 349 // restores) 350 // to perform the HFile bulk-load of the discovered store files into `targetTable`. 351 restoreService.run(pathsArray, new TableName[] { sourceTable }, new Path(restoreRootDir), 352 new TableName[] { targetTable }, false); 353 LOG.info("Re-bulkload completed for {}", targetTable); 354 } catch (Exception e) { 355 String errorMessage = 356 String.format("Re-bulkload failed for %s: %s", targetTable, e.getMessage()); 357 LOG.error(errorMessage, e); 358 throw new IOException(errorMessage, e); 359 } 360 } 361 362 /** 363 * Replays WALs to bring the table to the desired state. 364 */ 365 private void replayWal(TableName sourceTable, TableName targetTable, long startTime, long endTime) 366 throws IOException { 367 String walBackupDir = conn.getConfiguration().get(CONF_CONTINUOUS_BACKUP_WAL_DIR); 368 Path walDirPath = new Path(walBackupDir); 369 LOG.info( 370 "Starting WAL replay for source: {}, target: {}, time range: {} - {}, WAL backup dir: {}", 371 sourceTable, targetTable, startTime, endTime, walDirPath); 372 373 List<String> validDirs = 374 BackupUtils.getValidWalDirs(conn.getConfiguration(), walDirPath, startTime, endTime); 375 if (validDirs.isEmpty()) { 376 LOG.warn("No valid WAL directories found for range {} - {}. Skipping WAL replay.", startTime, 377 endTime); 378 return; 379 } 380 381 executeWalReplay(validDirs, sourceTable, targetTable, startTime, endTime); 382 } 383 384 /** 385 * Executes WAL replay using WALPlayer. 386 */ 387 private void executeWalReplay(List<String> walDirs, TableName sourceTable, TableName targetTable, 388 long startTime, long endTime) throws IOException { 389 Tool walPlayer = initializeWalPlayer(startTime, endTime); 390 String[] args = 391 { String.join(",", walDirs), sourceTable.getNameAsString(), targetTable.getNameAsString() }; 392 393 try { 394 LOG.info("Executing WALPlayer with args: {}", Arrays.toString(args)); 395 int exitCode = walPlayer.run(args); 396 if (exitCode == 0) { 397 LOG.info("WAL replay completed successfully for {}", targetTable); 398 } else { 399 throw new IOException("WAL replay failed with exit code: " + exitCode); 400 } 401 } catch (Exception e) { 402 LOG.error("Error during WAL replay for {}: {}", targetTable, e.getMessage(), e); 403 throw new IOException("Exception during WAL replay", e); 404 } 405 } 406 407 /** 408 * Initializes and configures WALPlayer. 409 */ 410 private Tool initializeWalPlayer(long startTime, long endTime) { 411 Configuration conf = HBaseConfiguration.create(conn.getConfiguration()); 412 conf.setLong(WALInputFormat.START_TIME_KEY, startTime); 413 conf.setLong(WALInputFormat.END_TIME_KEY, endTime); 414 conf.setBoolean(IGNORE_EMPTY_FILES, true); 415 // HFile output format defaults to false in HFileOutputFormat2, but we are explicitly setting 416 // it here just in case 417 conf.setBoolean(MULTI_TABLE_HFILEOUTPUTFORMAT_CONF_KEY, false); 418 Tool walPlayer = new WALPlayer(); 419 walPlayer.setConf(conf); 420 return walPlayer; 421 } 422 423 protected abstract List<PitrBackupMetadata> getBackupMetadata(PointInTimeRestoreRequest request) 424 throws IOException; 425}