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.util;
019
020import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONF_CONTINUOUS_BACKUP_WAL_DIR;
021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.CONTINUOUS_BACKUP_REPLICATION_PEER;
022import static org.apache.hadoop.hbase.backup.replication.ContinuousBackupReplicationEndpoint.ONE_DAY_IN_MILLISECONDS;
023import static org.apache.hadoop.hbase.backup.util.BackupFileSystemManager.WALS_DIR;
024import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_DEFAULT;
025import static org.apache.hadoop.hbase.replication.regionserver.ReplicationMarkerChore.REPLICATION_MARKER_ENABLED_KEY;
026
027import java.io.FileNotFoundException;
028import java.io.IOException;
029import java.net.URLDecoder;
030import java.text.ParseException;
031import java.text.SimpleDateFormat;
032import java.time.ZoneOffset;
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.Date;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Map.Entry;
041import java.util.TimeZone;
042import java.util.TreeSet;
043import java.util.function.Predicate;
044import java.util.stream.Collectors;
045import java.util.stream.Stream;
046import org.apache.hadoop.conf.Configuration;
047import org.apache.hadoop.fs.FSDataOutputStream;
048import org.apache.hadoop.fs.FileStatus;
049import org.apache.hadoop.fs.FileSystem;
050import org.apache.hadoop.fs.LocatedFileStatus;
051import org.apache.hadoop.fs.Path;
052import org.apache.hadoop.fs.PathFilter;
053import org.apache.hadoop.fs.RemoteIterator;
054import org.apache.hadoop.fs.permission.FsPermission;
055import org.apache.hadoop.hbase.HBaseConfiguration;
056import org.apache.hadoop.hbase.HConstants;
057import org.apache.hadoop.hbase.MetaTableAccessor;
058import org.apache.hadoop.hbase.ServerName;
059import org.apache.hadoop.hbase.TableName;
060import org.apache.hadoop.hbase.backup.BackupInfo;
061import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
062import org.apache.hadoop.hbase.backup.HBackupFileSystem;
063import org.apache.hadoop.hbase.backup.RestoreRequest;
064import org.apache.hadoop.hbase.backup.impl.BackupManifest;
065import org.apache.hadoop.hbase.backup.impl.BackupManifest.BackupImage;
066import org.apache.hadoop.hbase.backup.impl.BackupSystemTable;
067import org.apache.hadoop.hbase.backup.master.LogRollMasterProcedureManager;
068import org.apache.hadoop.hbase.client.Admin;
069import org.apache.hadoop.hbase.client.Connection;
070import org.apache.hadoop.hbase.client.RegionInfo;
071import org.apache.hadoop.hbase.client.TableDescriptor;
072import org.apache.hadoop.hbase.master.region.MasterRegionFactory;
073import org.apache.hadoop.hbase.replication.ReplicationException;
074import org.apache.hadoop.hbase.replication.ReplicationGroupOffset;
075import org.apache.hadoop.hbase.replication.ReplicationQueueId;
076import org.apache.hadoop.hbase.replication.ReplicationQueueStorage;
077import org.apache.hadoop.hbase.replication.ReplicationStorageFactory;
078import org.apache.hadoop.hbase.tool.BulkLoadHFiles;
079import org.apache.hadoop.hbase.util.CommonFSUtils;
080import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
081import org.apache.hadoop.hbase.util.FSTableDescriptors;
082import org.apache.hadoop.hbase.util.FSUtils;
083import org.apache.hadoop.hbase.wal.AbstractFSWALProvider;
084import org.apache.yetus.audience.InterfaceAudience;
085import org.slf4j.Logger;
086import org.slf4j.LoggerFactory;
087
088import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
089import org.apache.hbase.thirdparty.com.google.common.base.Strings;
090import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
091import org.apache.hbase.thirdparty.com.google.common.collect.Iterables;
092import org.apache.hbase.thirdparty.com.google.common.collect.Iterators;
093
094/**
095 * A collection for methods used by multiple classes to backup HBase tables.
096 */
097@InterfaceAudience.Private
098public final class BackupUtils {
099  private static final Logger LOG = LoggerFactory.getLogger(BackupUtils.class);
100  public static final String LOGNAME_SEPARATOR = ".";
101  public static final int MILLISEC_IN_HOUR = 3600000;
102  public static final String DATE_FORMAT = "yyyy-MM-dd";
103
104  private BackupUtils() {
105    throw new AssertionError("Instantiating utility class...");
106  }
107
108  /**
109   * Loop through the RS log timestamp map for the tables, for each RS, find the min timestamp value
110   * for the RS among the tables.
111   * @param rsLogTimestampMap timestamp map
112   * @return the min timestamp of each RS
113   */
114  public static Map<String, Long>
115    getRSLogTimestampMins(Map<TableName, Map<String, Long>> rsLogTimestampMap) {
116    return rsLogTimestampMap.values().stream().flatMap(map -> map.entrySet().stream())
117      .collect(Collectors.toMap(Entry::getKey, Entry::getValue, Math::min));
118  }
119
120  /**
121   * copy out Table RegionInfo into incremental backup image need to consider move this logic into
122   * HBackupFileSystem
123   * @param conn       connection
124   * @param backupInfo backup info
125   * @param conf       configuration
126   * @throws IOException exception
127   */
128  public static void copyTableRegionInfo(Connection conn, BackupInfo backupInfo, Configuration conf)
129    throws IOException {
130    Path rootDir = CommonFSUtils.getRootDir(conf);
131    FileSystem fs = rootDir.getFileSystem(conf);
132
133    // for each table in the table set, copy out the table info and region
134    // info files in the correct directory structure
135    try (Admin admin = conn.getAdmin()) {
136      for (TableName table : backupInfo.getTables()) {
137        if (!admin.tableExists(table)) {
138          LOG.warn("Table " + table + " does not exists, skipping it.");
139          continue;
140        }
141        TableDescriptor orig = FSTableDescriptors.getTableDescriptorFromFs(fs, rootDir, table);
142
143        // write a copy of descriptor to the target directory
144        Path target = new Path(backupInfo.getTableBackupDir(table));
145        FileSystem targetFs = target.getFileSystem(conf);
146        try (FSTableDescriptors descriptors =
147          new FSTableDescriptors(targetFs, CommonFSUtils.getRootDir(conf))) {
148          descriptors.createTableDescriptorForTableDirectory(target, orig, false);
149        }
150        LOG.debug("Attempting to copy table info for:" + table + " target: " + target
151          + " descriptor: " + orig);
152        LOG.debug("Finished copying tableinfo.");
153        List<RegionInfo> regions = MetaTableAccessor.getTableRegions(conn, table);
154        // For each region, write the region info to disk
155        LOG.debug("Starting to write region info for table " + table);
156        for (RegionInfo regionInfo : regions) {
157          Path regionDir = FSUtils
158            .getRegionDirFromTableDir(new Path(backupInfo.getTableBackupDir(table)), regionInfo);
159          regionDir = new Path(backupInfo.getTableBackupDir(table), regionDir.getName());
160          writeRegioninfoOnFilesystem(conf, targetFs, regionDir, regionInfo);
161        }
162        LOG.debug("Finished writing region info for table " + table);
163      }
164    }
165  }
166
167  /**
168   * Write the .regioninfo file on-disk.
169   */
170  public static void writeRegioninfoOnFilesystem(final Configuration conf, final FileSystem fs,
171    final Path regionInfoDir, RegionInfo regionInfo) throws IOException {
172    final byte[] content = RegionInfo.toDelimitedByteArray(regionInfo);
173    Path regionInfoFile = new Path(regionInfoDir, "." + HConstants.REGIONINFO_QUALIFIER_STR);
174    // First check to get the permissions
175    FsPermission perms = CommonFSUtils.getFilePermissions(fs, conf, HConstants.DATA_FILE_UMASK_KEY);
176    // Write the RegionInfo file content
177    FSDataOutputStream out = FSUtils.create(conf, fs, regionInfoFile, perms, null);
178    try {
179      out.write(content);
180    } finally {
181      out.close();
182    }
183  }
184
185  /**
186   * Parses hostname:port from WAL file path
187   * @param p path to WAL file
188   * @return hostname:port
189   */
190  public static String parseHostNameFromLogFile(Path p) {
191    try {
192      if (AbstractFSWALProvider.isArchivedLogFile(p)) {
193        return BackupUtils.parseHostFromOldLog(p);
194      } else {
195        ServerName sname = AbstractFSWALProvider.getServerNameFromWALDirectoryName(p);
196        if (sname != null) {
197          return sname.getAddress().toString();
198        } else {
199          LOG.error("Skip log file (can't parse): " + p);
200          return null;
201        }
202      }
203    } catch (Exception e) {
204      LOG.error("Skip log file (can't parse): " + p, e);
205      return null;
206    }
207  }
208
209  /**
210   * Returns WAL file name
211   * @param walFileName WAL file name
212   * @return WAL file name
213   */
214  public static String getUniqueWALFileNamePart(String walFileName) {
215    return getUniqueWALFileNamePart(new Path(walFileName));
216  }
217
218  /**
219   * Returns WAL file name
220   * @param p WAL file path
221   * @return WAL file name
222   */
223  public static String getUniqueWALFileNamePart(Path p) {
224    return p.getName();
225  }
226
227  /**
228   * Get the total length of files under the given directory recursively.
229   * @param fs  The hadoop file system
230   * @param dir The target directory
231   * @return the total length of files
232   * @throws IOException exception
233   */
234  public static long getFilesLength(FileSystem fs, Path dir) throws IOException {
235    long totalLength = 0;
236    FileStatus[] files = CommonFSUtils.listStatus(fs, dir);
237    if (files != null) {
238      for (FileStatus fileStatus : files) {
239        if (fileStatus.isDirectory()) {
240          totalLength += getFilesLength(fs, fileStatus.getPath());
241        } else {
242          totalLength += fileStatus.getLen();
243        }
244      }
245    }
246    return totalLength;
247  }
248
249  /**
250   * Get list of all old WAL files (WALs and archive)
251   * @param c                configuration
252   * @param hostTimestampMap {host,timestamp} map
253   * @return list of WAL files
254   * @throws IOException exception
255   */
256  public static List<String> getWALFilesOlderThan(final Configuration c,
257    final HashMap<String, Long> hostTimestampMap) throws IOException {
258    Path walRootDir = CommonFSUtils.getWALRootDir(c);
259    Path logDir = new Path(walRootDir, HConstants.HREGION_LOGDIR_NAME);
260    Path oldLogDir = new Path(walRootDir, HConstants.HREGION_OLDLOGDIR_NAME);
261    List<String> logFiles = new ArrayList<>();
262
263    PathFilter filter = p -> {
264      try {
265        if (AbstractFSWALProvider.isMetaFile(p)) {
266          return false;
267        }
268        String host = parseHostNameFromLogFile(p);
269        if (host == null) {
270          return false;
271        }
272        Long oldTimestamp = hostTimestampMap.get(host);
273        Long currentLogTS = BackupUtils.getCreationTime(p);
274        return currentLogTS <= oldTimestamp;
275      } catch (Exception e) {
276        LOG.warn("Can not parse" + p, e);
277        return false;
278      }
279    };
280    FileSystem walFs = CommonFSUtils.getWALFileSystem(c);
281    logFiles = BackupUtils.getFiles(walFs, logDir, logFiles, filter);
282    logFiles = BackupUtils.getFiles(walFs, oldLogDir, logFiles, filter);
283    return logFiles;
284  }
285
286  public static TableName[] parseTableNames(String tables) {
287    if (tables == null) {
288      return null;
289    }
290    return Splitter.on(BackupRestoreConstants.TABLENAME_DELIMITER_IN_COMMAND).splitToStream(tables)
291      .map(TableName::valueOf).toArray(TableName[]::new);
292  }
293
294  /**
295   * Check whether the backup path exist
296   * @param backupStr backup
297   * @param conf      configuration
298   * @return Yes if path exists
299   * @throws IOException exception
300   */
301  public static boolean checkPathExist(String backupStr, Configuration conf) throws IOException {
302    boolean isExist = false;
303    Path backupPath = new Path(backupStr);
304    FileSystem fileSys = backupPath.getFileSystem(conf);
305    String targetFsScheme = fileSys.getUri().getScheme();
306    if (LOG.isTraceEnabled()) {
307      LOG.trace("Schema of given url: " + backupStr + " is: " + targetFsScheme);
308    }
309    if (fileSys.exists(backupPath)) {
310      isExist = true;
311    }
312    return isExist;
313  }
314
315  /**
316   * Check target path first, confirm it doesn't exist before backup
317   * @param backupRootPath backup destination path
318   * @param conf           configuration
319   * @throws IOException exception
320   */
321  public static void checkTargetDir(String backupRootPath, Configuration conf) throws IOException {
322    boolean targetExists;
323    try {
324      targetExists = checkPathExist(backupRootPath, conf);
325    } catch (IOException e) {
326      String expMsg = e.getMessage();
327      String newMsg = null;
328      if (expMsg.contains("No FileSystem for scheme")) {
329        newMsg =
330          "Unsupported filesystem scheme found in the backup target url. Error Message: " + expMsg;
331        LOG.error(newMsg);
332        throw new IOException(newMsg);
333      } else {
334        throw e;
335      }
336    }
337
338    if (targetExists) {
339      LOG.info("Using existing backup root dir: " + backupRootPath);
340    } else {
341      LOG.info("Backup root dir " + backupRootPath + " does not exist. Will be created.");
342    }
343  }
344
345  /**
346   * Get the min value for all the Values a map.
347   * @param map map
348   * @return the min value
349   */
350  public static <T> Long getMinValue(Map<T, Long> map) {
351    Long minTimestamp = null;
352    if (map != null) {
353      ArrayList<Long> timestampList = new ArrayList<>(map.values());
354      Collections.sort(timestampList);
355      // The min among all the RS log timestamps will be kept in backup system table table.
356      minTimestamp = timestampList.get(0);
357    }
358    return minTimestamp;
359  }
360
361  /**
362   * Parses host name:port from archived WAL path
363   * @param p path
364   * @return host name
365   */
366  public static String parseHostFromOldLog(Path p) {
367    // Skip master wals
368    if (p.getName().endsWith(MasterRegionFactory.ARCHIVED_WAL_SUFFIX)) {
369      return null;
370    }
371    Path parent = p.getParent();
372    if (parent != null && ServerName.isFullServerName(parent.getName())) {
373      return ServerName.valueOf(parent.getName()).getAddress().toString();
374    }
375    try {
376      String urlDecodedName = URLDecoder.decode(p.getName(), "UTF8");
377      Iterable<String> nameSplitsOnComma = Splitter.on(",").split(urlDecodedName);
378      String host = Iterables.get(nameSplitsOnComma, 0);
379      String port = Iterables.get(nameSplitsOnComma, 1);
380      return host + ":" + port;
381    } catch (Exception e) {
382      LOG.warn("Skip log file (can't parse): {}", p);
383      return null;
384    }
385  }
386
387  /**
388   * Given the log file, parse the timestamp from the file name. The timestamp is the last number.
389   * @param p a path to the log file
390   * @return the timestamp
391   * @throws IOException exception
392   */
393  public static Long getCreationTime(Path p) throws IOException {
394    int idx = p.getName().lastIndexOf(LOGNAME_SEPARATOR);
395    if (idx < 0) {
396      throw new IOException("Cannot parse timestamp from path " + p);
397    }
398    String ts = p.getName().substring(idx + 1);
399    return Long.parseLong(ts);
400  }
401
402  public static List<String> getFiles(FileSystem fs, Path rootDir, List<String> files,
403    PathFilter filter) throws IOException {
404    RemoteIterator<LocatedFileStatus> it = fs.listFiles(rootDir, true);
405
406    while (it.hasNext()) {
407      LocatedFileStatus lfs = it.next();
408      if (lfs.isDirectory()) {
409        continue;
410      }
411      // apply filter
412      if (filter.accept(lfs.getPath())) {
413        files.add(lfs.getPath().toString());
414      }
415    }
416    return files;
417  }
418
419  public static void cleanupBackupData(BackupInfo context, Configuration conf) throws IOException {
420    cleanupHLogDir(context, conf);
421    cleanupTargetDir(context, conf);
422  }
423
424  /**
425   * Clean up directories which are generated when DistCp copying hlogs
426   * @param backupInfo backup info
427   * @param conf       configuration
428   * @throws IOException exception
429   */
430  private static void cleanupHLogDir(BackupInfo backupInfo, Configuration conf) throws IOException {
431    String logDir = backupInfo.getHLogTargetDir();
432    if (logDir == null) {
433      LOG.warn("No log directory specified for " + backupInfo.getBackupId());
434      return;
435    }
436
437    Path rootPath = new Path(logDir).getParent();
438    FileSystem fs = FileSystem.get(rootPath.toUri(), conf);
439    FileStatus[] files = listStatus(fs, rootPath, null);
440    if (files == null) {
441      return;
442    }
443    for (FileStatus file : files) {
444      LOG.debug("Delete log files: " + file.getPath().getName());
445      fs.delete(file.getPath(), true);
446    }
447  }
448
449  private static void cleanupTargetDir(BackupInfo backupInfo, Configuration conf) {
450    try {
451      // clean up the data at target directory
452      LOG.debug("Trying to cleanup up target dir : " + backupInfo.getBackupId());
453      String targetDir = backupInfo.getBackupRootDir();
454      if (targetDir == null) {
455        LOG.warn("No target directory specified for " + backupInfo.getBackupId());
456        return;
457      }
458
459      FileSystem outputFs = FileSystem.get(new Path(backupInfo.getBackupRootDir()).toUri(), conf);
460
461      for (TableName table : backupInfo.getTables()) {
462        Path targetDirPath = new Path(
463          getTableBackupDir(backupInfo.getBackupRootDir(), backupInfo.getBackupId(), table));
464        if (outputFs.delete(targetDirPath, true)) {
465          LOG.info("Cleaning up backup data at " + targetDirPath.toString() + " done.");
466        } else {
467          LOG.info("No data has been found in " + targetDirPath.toString() + ".");
468        }
469
470        Path tableDir = targetDirPath.getParent();
471        FileStatus[] backups = listStatus(outputFs, tableDir, null);
472        if (backups == null || backups.length == 0) {
473          outputFs.delete(tableDir, true);
474          LOG.debug(tableDir.toString() + " is empty, remove it.");
475        }
476      }
477      outputFs.delete(new Path(targetDir, backupInfo.getBackupId()), true);
478    } catch (IOException e1) {
479      LOG.error("Cleaning up backup data of " + backupInfo.getBackupId() + " at "
480        + backupInfo.getBackupRootDir() + " failed due to " + e1.getMessage() + ".");
481    }
482  }
483
484  /**
485   * Given the backup root dir, backup id and the table name, return the backup image location,
486   * which is also where the backup manifest file is. return value look like:
487   * "hdfs://backup.hbase.org:9000/user/biadmin/backup1/backup_1396650096738/default/t1_dn/"
488   * @param backupRootDir backup root directory
489   * @param backupId      backup id
490   * @param tableName     table name
491   * @return backupPath String for the particular table
492   */
493  public static String getTableBackupDir(String backupRootDir, String backupId,
494    TableName tableName) {
495    return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR
496      + tableName.getNamespaceAsString() + Path.SEPARATOR + tableName.getQualifierAsString()
497      + Path.SEPARATOR;
498  }
499
500  /**
501   * Calls fs.listStatus() and treats FileNotFoundException as non-fatal This accommodates
502   * differences between hadoop versions, where hadoop 1 does not throw a FileNotFoundException, and
503   * return an empty FileStatus[] while Hadoop 2 will throw FileNotFoundException.
504   * @param fs     file system
505   * @param dir    directory
506   * @param filter path filter
507   * @return null if dir is empty or doesn't exist, otherwise FileStatus array
508   */
509  public static FileStatus[] listStatus(final FileSystem fs, final Path dir,
510    final PathFilter filter) throws IOException {
511    FileStatus[] status = null;
512    try {
513      status = filter == null ? fs.listStatus(dir) : fs.listStatus(dir, filter);
514    } catch (FileNotFoundException fnfe) {
515      // if directory doesn't exist, return null
516      if (LOG.isTraceEnabled()) {
517        LOG.trace(dir + " doesn't exist");
518      }
519    }
520
521    if (status == null || status.length < 1) {
522      return null;
523    }
524
525    return status;
526  }
527
528  /**
529   * Return the 'path' component of a Path. In Hadoop, Path is a URI. This method returns the 'path'
530   * component of a Path's URI: e.g. If a Path is
531   * <code>hdfs://example.org:9000/hbase_trunk/TestTable/compaction.dir</code>, this method returns
532   * <code>/hbase_trunk/TestTable/compaction.dir</code>. This method is useful if you want to print
533   * out a Path without qualifying Filesystem instance.
534   * @param p file system Path whose 'path' component we are to return.
535   * @return Path portion of the Filesystem
536   */
537  public static String getPath(Path p) {
538    return p.toUri().getPath();
539  }
540
541  /**
542   * Given the backup root dir and the backup id, return the log file location for an incremental
543   * backup.
544   * @param backupRootDir backup root directory
545   * @param backupId      backup id
546   * @return logBackupDir: ".../user/biadmin/backup1/WALs/backup_1396650096738"
547   */
548  public static String getLogBackupDir(String backupRootDir, String backupId) {
549    return backupRootDir + Path.SEPARATOR + backupId + Path.SEPARATOR
550      + HConstants.HREGION_LOGDIR_NAME;
551  }
552
553  /**
554   * Loads all backup history as stored in files on the given backup root path.
555   * @return all backup history, from newest (most recent) to oldest (least recent)
556   */
557  private static List<BackupInfo> getHistory(Configuration conf, Path backupRootPath)
558    throws IOException {
559    // Get all (n) history from backup root destination
560
561    FileSystem fs = FileSystem.get(backupRootPath.toUri(), conf);
562    RemoteIterator<LocatedFileStatus> it;
563    try {
564      it = fs.listLocatedStatus(backupRootPath);
565    } catch (FileNotFoundException e) {
566      return Collections.emptyList();
567    }
568
569    List<BackupInfo> infos = new ArrayList<>();
570    while (it.hasNext()) {
571      LocatedFileStatus lfs = it.next();
572
573      if (!lfs.isDirectory()) {
574        continue;
575      }
576
577      String backupId = lfs.getPath().getName();
578      try {
579        BackupInfo info = loadBackupInfo(backupRootPath, backupId, fs);
580        infos.add(info);
581      } catch (IOException e) {
582        LOG.error("Can not load backup info from: " + lfs.getPath(), e);
583      }
584    }
585    // Sort
586    infos.sort(Comparator.<BackupInfo> naturalOrder().reversed());
587    return infos;
588  }
589
590  /**
591   * Loads all backup history as stored in files on the given backup root path, and returns the
592   * first n entries matching all given filters.
593   * @return (subset of) backup history, from newest (most recent) to oldest (least recent)
594   */
595  public static List<BackupInfo> getHistory(Configuration conf, int n, Path backupRootPath,
596    BackupInfo.Filter... filters) throws IOException {
597    List<BackupInfo> infos = getHistory(conf, backupRootPath);
598
599    Predicate<BackupInfo> combinedPredicate = Stream.of(filters)
600      .map(filter -> (Predicate<BackupInfo>) filter).reduce(Predicate::and).orElse(x -> true);
601
602    return infos.stream().filter(combinedPredicate).limit(n).toList();
603  }
604
605  public static BackupInfo loadBackupInfo(Path backupRootPath, String backupId, FileSystem fs)
606    throws IOException {
607    Path backupPath = new Path(backupRootPath, backupId);
608
609    RemoteIterator<LocatedFileStatus> it = fs.listFiles(backupPath, true);
610    while (it.hasNext()) {
611      LocatedFileStatus lfs = it.next();
612      if (lfs.getPath().getName().equals(BackupManifest.MANIFEST_FILE_NAME)) {
613        // Load BackupManifest
614        BackupManifest manifest = new BackupManifest(fs, lfs.getPath().getParent());
615        BackupInfo info = manifest.toBackupInfo();
616        return info;
617      }
618    }
619    return null;
620  }
621
622  /**
623   * Create restore request.
624   * @param backupRootDir backup root dir
625   * @param backupId      backup id
626   * @param check         check only
627   * @param fromTables    table list from
628   * @param toTables      table list to
629   * @param isOverwrite   overwrite data
630   * @return request obkect
631   */
632  public static RestoreRequest createRestoreRequest(String backupRootDir, String backupId,
633    boolean check, TableName[] fromTables, TableName[] toTables, boolean isOverwrite) {
634    return createRestoreRequest(backupRootDir, backupId, check, fromTables, toTables, isOverwrite,
635      false);
636  }
637
638  public static RestoreRequest createRestoreRequest(String backupRootDir, String backupId,
639    boolean check, TableName[] fromTables, TableName[] toTables, boolean isOverwrite,
640    boolean isKeepOriginalSplits) {
641    RestoreRequest.Builder builder = new RestoreRequest.Builder();
642    RestoreRequest request = builder.withBackupRootDir(backupRootDir).withBackupId(backupId)
643      .withCheck(check).withFromTables(fromTables).withToTables(toTables).withOverwrite(isOverwrite)
644      .withKeepOriginalSplits(isKeepOriginalSplits).build();
645    return request;
646  }
647
648  public static boolean validate(List<TableName> tables, BackupManifest backupManifest,
649    Configuration conf) throws IOException {
650    boolean isValid = true;
651
652    for (TableName table : tables) {
653      TreeSet<BackupImage> imageSet = new TreeSet<>();
654
655      ArrayList<BackupImage> depList = backupManifest.getDependentListByTable(table);
656      if (depList != null && !depList.isEmpty()) {
657        imageSet.addAll(depList);
658      }
659
660      LOG.info("Dependent image(s) from old to new:");
661      for (BackupImage image : imageSet) {
662        String imageDir =
663          HBackupFileSystem.getTableBackupDir(image.getRootDir(), image.getBackupId(), table);
664        if (!BackupUtils.checkPathExist(imageDir, conf)) {
665          LOG.error("ERROR: backup image does not exist: " + imageDir);
666          isValid = false;
667          break;
668        }
669        LOG.info("Backup image: " + image.getBackupId() + " for '" + table + "' is available");
670      }
671    }
672    return isValid;
673  }
674
675  public static Path getBulkOutputDir(Path restoreRootDir, String tableName, Configuration conf,
676    boolean deleteOnExit) throws IOException {
677    FileSystem fs = restoreRootDir.getFileSystem(conf);
678    Path path = new Path(restoreRootDir,
679      "bulk_output-" + tableName + "-" + EnvironmentEdgeManager.currentTime());
680    if (deleteOnExit) {
681      fs.deleteOnExit(path);
682    }
683    return path;
684  }
685
686  public static Path getBulkOutputDir(Path restoreRootDir, String tableName, Configuration conf)
687    throws IOException {
688    return getBulkOutputDir(restoreRootDir, tableName, conf, true);
689  }
690
691  public static Path getBulkOutputDir(String tableName, Configuration conf, boolean deleteOnExit)
692    throws IOException {
693    FileSystem fs = FileSystem.get(conf);
694    return getBulkOutputDir(getTmpRestoreOutputDir(fs, conf), tableName, conf, deleteOnExit);
695  }
696
697  /**
698   * Build temporary output path
699   * @param fs   filesystem for default output dir
700   * @param conf configuration
701   * @return output path
702   */
703  public static Path getTmpRestoreOutputDir(FileSystem fs, Configuration conf) {
704    String tmp =
705      conf.get(HConstants.TEMPORARY_FS_DIRECTORY_KEY, fs.getHomeDirectory() + "/hbase-staging");
706    return new Path(tmp);
707  }
708
709  public static String getFileNameCompatibleString(TableName table) {
710    return table.getNamespaceAsString() + "-" + table.getQualifierAsString();
711  }
712
713  public static boolean failed(int result) {
714    return result != 0;
715  }
716
717  public static boolean succeeded(int result) {
718    return result == 0;
719  }
720
721  public static BulkLoadHFiles createLoader(Configuration config) {
722    // set configuration for restore:
723    // LoadIncrementalHFile needs more time
724    // <name>hbase.rpc.timeout</name> <value>600000</value>
725    // calculates
726    Configuration conf = new Configuration(config);
727    conf.setInt(HConstants.HBASE_RPC_TIMEOUT_KEY, MILLISEC_IN_HOUR);
728
729    // By default, it is 32 and loader will fail if # of files in any region exceed this
730    // limit. Bad for snapshot restore.
731    conf.setInt(BulkLoadHFiles.MAX_FILES_PER_REGION_PER_FAMILY, Integer.MAX_VALUE);
732    conf.set(BulkLoadHFiles.IGNORE_UNMATCHED_CF_CONF_KEY, "yes");
733    return BulkLoadHFiles.create(conf);
734  }
735
736  public static String findMostRecentBackupId(String[] backupIds) {
737    long recentTimestamp = Long.MIN_VALUE;
738    for (String backupId : backupIds) {
739      long ts = Long.parseLong(Iterators.get(Splitter.on('_').split(backupId).iterator(), 1));
740      if (ts > recentTimestamp) {
741        recentTimestamp = ts;
742      }
743    }
744    return BackupRestoreConstants.BACKUPID_PREFIX + recentTimestamp;
745  }
746
747  /**
748   * roll WAL writer for all region servers and record the newest log roll result
749   */
750  public static void logRoll(Connection conn, String backupRootDir, Configuration conf)
751    throws IOException {
752    boolean legacy = conf.getBoolean("hbase.backup.logroll.legacy.used", false);
753    if (legacy) {
754      logRollV1(conn, backupRootDir);
755    } else {
756      logRollV2(conn, backupRootDir);
757    }
758  }
759
760  private static void logRollV1(Connection conn, String backupRootDir) throws IOException {
761    try (Admin admin = conn.getAdmin()) {
762      admin.execProcedure(LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_SIGNATURE,
763        LogRollMasterProcedureManager.ROLLLOG_PROCEDURE_NAME,
764        ImmutableMap.of("backupRoot", backupRootDir));
765    }
766  }
767
768  private static void logRollV2(Connection conn, String backupRootDir) throws IOException {
769    BackupSystemTable backupSystemTable = new BackupSystemTable(conn);
770    HashMap<String, Long> lastLogRollResult =
771      backupSystemTable.readRegionServerLastLogRollResult(backupRootDir);
772    try (Admin admin = conn.getAdmin()) {
773      Map<ServerName, Long> newLogRollResult = admin.rollAllWALWriters();
774
775      for (Map.Entry<ServerName, Long> entry : newLogRollResult.entrySet()) {
776        ServerName serverName = entry.getKey();
777        long newHighestWALFilenum = entry.getValue();
778
779        String address = serverName.getAddress().toString();
780        Long lastHighestWALFilenum = lastLogRollResult.get(address);
781        if (lastHighestWALFilenum != null && lastHighestWALFilenum > newHighestWALFilenum) {
782          LOG.warn("Won't update last roll log result for server {}: current = {}, new = {}",
783            serverName, lastHighestWALFilenum, newHighestWALFilenum);
784        } else {
785          backupSystemTable.writeRegionServerLastLogRollResult(address, newHighestWALFilenum,
786            backupRootDir);
787          if (LOG.isDebugEnabled()) {
788            LOG.debug("updated last roll log result for {} from {} to {}", serverName,
789              lastHighestWALFilenum, newHighestWALFilenum);
790          }
791        }
792      }
793    }
794  }
795
796  /**
797   * Calculates the replication checkpoint timestamp used for continuous backup.
798   * <p>
799   * A replication checkpoint is the earliest timestamp across all region servers such that every
800   * WAL entry before that point is known to be replicated to the target system. This is essential
801   * for features like Point-in-Time Restore (PITR) and incremental backups, where we want to
802   * confidently restore data to a consistent state without missing updates.
803   * <p>
804   * The checkpoint is calculated using a combination of:
805   * <ul>
806   * <li>The start timestamps of WAL files currently being replicated for each server.</li>
807   * <li>The latest successfully replicated timestamp recorded by the replication marker chore.</li>
808   * </ul>
809   * <p>
810   * We combine these two sources to handle the following challenges:
811   * <ul>
812   * <li><b>Stale WAL start times:</b> If replication traffic is low or WALs are long-lived, the
813   * replication offset may point to the same WAL for a long time, resulting in stale timestamps
814   * that underestimate progress. This could delay PITR unnecessarily.</li>
815   * <li><b>Limitations of marker-only tracking:</b> The replication marker chore stores the last
816   * successfully replicated timestamp per region server in a system table. However, this data may
817   * become stale if the server goes offline or region ownership changes. For example, if a region
818   * initially belonged to rs1 and was later moved to rs4 due to re-balancing, rs1’s marker would
819   * persist even though it no longer holds any regions. Relying solely on these stale markers could
820   * lead to incorrect or outdated checkpoints.</li>
821   * </ul>
822   * <p>
823   * To handle these limitations, the method:
824   * <ol>
825   * <li>Verifies that the continuous backup peer exists to ensure replication is enabled.</li>
826   * <li>Retrieves WAL replication queue information for the peer, collecting WAL start times per
827   * region server. This gives us a lower bound for replication progress.</li>
828   * <li>Reads the marker chore's replicated timestamps from the backup system table.</li>
829   * <li>For servers found in both sources, if the marker timestamp is more recent than the WAL's
830   * start timestamp, we use the marker (since replication has progressed beyond the WAL).</li>
831   * <li>We discard marker entries for region servers that are not present in WAL queues, assuming
832   * those servers are no longer relevant (e.g., decommissioned or reassigned).</li>
833   * <li>The checkpoint is the minimum of all chosen timestamps — i.e., the slowest replicating
834   * region server.</li>
835   * <li>Finally, we persist the updated marker information to include any newly participating
836   * region servers.</li>
837   * </ol>
838   * <p>
839   * Note: If the replication marker chore is disabled, we fall back to using only the WAL start
840   * times. This ensures correctness but may lead to conservative checkpoint estimates during idle
841   * periods.
842   * @param conn the HBase connection
843   * @return the calculated replication checkpoint timestamp
844   * @throws IOException if reading replication queues or updating the backup system table fails
845   */
846  public static long getReplicationCheckpoint(Connection conn) throws IOException {
847    Configuration conf = conn.getConfiguration();
848    long checkpoint = EnvironmentEdgeManager.getDelegate().currentTime();
849
850    // Step 1: Ensure the continuous backup replication peer exists
851    if (!continuousBackupReplicationPeerExists(conn.getAdmin())) {
852      String msg = "Replication peer '" + CONTINUOUS_BACKUP_REPLICATION_PEER
853        + "' not found. Continuous backup not enabled.";
854      LOG.error(msg);
855      throw new IOException(msg);
856    }
857
858    // Step 2: Get all replication queues for the continuous backup peer
859    ReplicationQueueStorage queueStorage =
860      ReplicationStorageFactory.getReplicationQueueStorage(conn, conf);
861
862    List<ReplicationQueueId> queueIds;
863    try {
864      queueIds = queueStorage.listAllQueueIds(CONTINUOUS_BACKUP_REPLICATION_PEER);
865    } catch (ReplicationException e) {
866      String msg = "Failed to retrieve replication queue IDs for peer '"
867        + CONTINUOUS_BACKUP_REPLICATION_PEER + "'";
868      LOG.error(msg, e);
869      throw new IOException(msg, e);
870    }
871
872    if (queueIds.isEmpty()) {
873      String msg = "Replication peer '" + CONTINUOUS_BACKUP_REPLICATION_PEER + "' has no queues. "
874        + "This may indicate that continuous backup replication is not initialized correctly.";
875      LOG.error(msg);
876      throw new IOException(msg);
877    }
878
879    // Step 3: Build a map of ServerName -> WAL start timestamp (lowest seen per server)
880    Map<ServerName, Long> serverToCheckpoint = new HashMap<>();
881    for (ReplicationQueueId queueId : queueIds) {
882      Map<String, ReplicationGroupOffset> offsets;
883      try {
884        offsets = queueStorage.getOffsets(queueId);
885      } catch (ReplicationException e) {
886        String msg = "Failed to fetch WAL offsets for replication queue: " + queueId;
887        LOG.error(msg, e);
888        throw new IOException(msg, e);
889      }
890
891      for (ReplicationGroupOffset offset : offsets.values()) {
892        String walFile = offset.getWal();
893        long ts = AbstractFSWALProvider.getTimestamp(walFile); // WAL creation time
894        ServerName server = queueId.getServerName();
895        // Store the minimum timestamp per server (ts - 1 to avoid edge boundary issues)
896        serverToCheckpoint.merge(server, ts - 1, Math::min);
897      }
898    }
899
900    // Step 4: If replication markers are enabled, overlay fresher timestamps from backup system
901    // table
902    boolean replicationMarkerEnabled =
903      conf.getBoolean(REPLICATION_MARKER_ENABLED_KEY, REPLICATION_MARKER_ENABLED_DEFAULT);
904    if (replicationMarkerEnabled) {
905      try (BackupSystemTable backupSystemTable = new BackupSystemTable(conn)) {
906        Map<ServerName, Long> markerTimestamps = backupSystemTable.getBackupCheckpointTimestamps();
907
908        for (Map.Entry<ServerName, Long> entry : markerTimestamps.entrySet()) {
909          ServerName server = entry.getKey();
910          long markerTs = entry.getValue();
911
912          // If marker timestamp is newer, override
913          if (serverToCheckpoint.containsKey(server)) {
914            long current = serverToCheckpoint.get(server);
915            if (markerTs > current) {
916              serverToCheckpoint.put(server, markerTs);
917            }
918          } else {
919            // This server is no longer active (e.g., RS moved or removed); skip
920            if (LOG.isDebugEnabled()) {
921              LOG.debug("Skipping replication marker timestamp for inactive server: {}", server);
922            }
923          }
924        }
925
926        // Step 5: Persist current server timestamps into backup system table
927        for (Map.Entry<ServerName, Long> entry : serverToCheckpoint.entrySet()) {
928          backupSystemTable.updateBackupCheckpointTimestamp(entry.getKey(), entry.getValue());
929        }
930      }
931    } else {
932      LOG.warn(
933        "Replication marker chore is disabled. Using WAL-based timestamps only for checkpoint calculation.");
934    }
935
936    // Step 6: Calculate final checkpoint as minimum timestamp across all active servers
937    for (long ts : serverToCheckpoint.values()) {
938      checkpoint = Math.min(checkpoint, ts);
939    }
940
941    return checkpoint;
942  }
943
944  private static boolean continuousBackupReplicationPeerExists(Admin admin) throws IOException {
945    return admin.listReplicationPeers().stream()
946      .anyMatch(peer -> peer.getPeerId().equals(CONTINUOUS_BACKUP_REPLICATION_PEER));
947  }
948
949  /**
950   * Convert dayInMillis to "yyyy-MM-dd" format
951   */
952  public static String formatToDateString(long dayInMillis) {
953    SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
954    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
955    return dateFormat.format(new Date(dayInMillis));
956  }
957
958  /**
959   * Fetches bulkload filepaths based on the given time range from backup WAL directory.
960   */
961  public static List<Path> collectBulkFiles(Connection conn, TableName sourceTable,
962    TableName targetTable, long startTime, long endTime, Path restoreRootDir, List<String> walDirs)
963    throws IOException {
964
965    if (walDirs.isEmpty()) {
966      String walBackupDir = conn.getConfiguration().get(CONF_CONTINUOUS_BACKUP_WAL_DIR);
967      if (Strings.isNullOrEmpty(walBackupDir)) {
968        throw new IOException(
969          "WAL backup directory is not configured " + CONF_CONTINUOUS_BACKUP_WAL_DIR);
970      }
971      Path walDirPath = new Path(walBackupDir);
972      walDirs =
973        BackupUtils.getValidWalDirs(conn.getConfiguration(), walDirPath, startTime, endTime);
974    }
975
976    if (walDirs.isEmpty()) {
977      LOG.warn("No valid WAL directories found for range {} - {}. Skipping bulk-file collection.",
978        startTime, endTime);
979      return Collections.emptyList();
980    }
981
982    LOG.info(
983      "Starting WAL bulk-file collection for source: {}, target: {}, time range: {} - {}, WAL "
984        + "backup dir: {}, restore root: {}",
985      sourceTable, targetTable, startTime, endTime, walDirs, restoreRootDir);
986    String walDirsCsv = String.join(",", walDirs);
987
988    return BulkFilesCollector.collectFromWalDirs(HBaseConfiguration.create(conn.getConfiguration()),
989      walDirsCsv, restoreRootDir, sourceTable, targetTable, startTime, endTime);
990  }
991
992  /**
993   * Fetches valid WAL directories based on the given time range.
994   */
995  public static List<String> getValidWalDirs(Configuration conf, Path walBackupDir, long startTime,
996    long endTime) throws IOException {
997    FileSystem backupFs = FileSystem.get(walBackupDir.toUri(), conf);
998    FileStatus[] dayDirs = backupFs.listStatus(new Path(walBackupDir, WALS_DIR));
999
1000    List<String> validDirs = new ArrayList<>();
1001    SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
1002    dateFormat.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC));
1003
1004    for (FileStatus dayDir : dayDirs) {
1005      if (!dayDir.isDirectory()) {
1006        continue; // Skip files, only process directories
1007      }
1008
1009      String dirName = dayDir.getPath().getName();
1010      try {
1011        Date dirDate = dateFormat.parse(dirName);
1012        long dirStartTime = dirDate.getTime(); // Start of that day (00:00:00)
1013        long dirEndTime = dirStartTime + ONE_DAY_IN_MILLISECONDS - 1; // End time of day (23:59:59)
1014
1015        // Check if this day's WAL files overlap with the required time range
1016        if (dirEndTime >= startTime && dirStartTime <= endTime) {
1017          validDirs.add(dayDir.getPath().toString());
1018        }
1019      } catch (ParseException e) {
1020        LOG.warn("Skipping invalid directory name: {}", dirName, e);
1021      }
1022    }
1023    return validDirs;
1024  }
1025}