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