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}