001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.backup.impl;
019
020import static org.apache.hadoop.hbase.backup.BackupInfo.withRoot;
021import static org.apache.hadoop.hbase.backup.BackupInfo.withState;
022import static org.apache.hadoop.hbase.backup.BackupInfo.withType;
023
024import edu.umd.cs.findbugs.annotations.Nullable;
025import java.io.Closeable;
026import java.io.IOException;
027import java.io.InterruptedIOException;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Map;
038import java.util.Map.Entry;
039import java.util.Objects;
040import java.util.Set;
041import java.util.TreeMap;
042import java.util.TreeSet;
043import java.util.function.Predicate;
044import java.util.stream.Collectors;
045import java.util.stream.Stream;
046import org.apache.commons.lang3.ArrayUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.hadoop.conf.Configuration;
049import org.apache.hadoop.fs.Path;
050import org.apache.hadoop.hbase.Cell;
051import org.apache.hadoop.hbase.CellUtil;
052import org.apache.hadoop.hbase.HBaseConfiguration;
053import org.apache.hadoop.hbase.NamespaceDescriptor;
054import org.apache.hadoop.hbase.NamespaceExistException;
055import org.apache.hadoop.hbase.ServerName;
056import org.apache.hadoop.hbase.TableExistsException;
057import org.apache.hadoop.hbase.TableName;
058import org.apache.hadoop.hbase.TableNotDisabledException;
059import org.apache.hadoop.hbase.backup.BackupInfo;
060import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
061import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
062import org.apache.hadoop.hbase.backup.BackupType;
063import org.apache.hadoop.hbase.client.Admin;
064import org.apache.hadoop.hbase.client.BufferedMutator;
065import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
066import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
067import org.apache.hadoop.hbase.client.Connection;
068import org.apache.hadoop.hbase.client.Delete;
069import org.apache.hadoop.hbase.client.Get;
070import org.apache.hadoop.hbase.client.Put;
071import org.apache.hadoop.hbase.client.Result;
072import org.apache.hadoop.hbase.client.ResultScanner;
073import org.apache.hadoop.hbase.client.Scan;
074import org.apache.hadoop.hbase.client.SnapshotDescription;
075import org.apache.hadoop.hbase.client.Table;
076import org.apache.hadoop.hbase.client.TableDescriptor;
077import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
078import org.apache.hadoop.hbase.util.Bytes;
079import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
080import org.apache.yetus.audience.InterfaceAudience;
081import org.slf4j.Logger;
082import org.slf4j.LoggerFactory;
083
084import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
085import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
086import org.apache.hbase.thirdparty.com.google.common.collect.Iterators;
087
088import org.apache.hadoop.hbase.shaded.protobuf.generated.BackupProtos;
089import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
090
091/**
092 * This class provides API to access backup system table<br>
093 * Backup system table schema:<br>
094 * <p>
095 * <ul>
096 * <li>1. Backup sessions rowkey= "session:"+backupId; value =serialized BackupInfo</li>
097 * <li>2. Backup start code rowkey = "startcode:"+backupRoot; value = startcode</li>
098 * <li>3. Incremental backup set rowkey="incrbackupset:"+backupRoot; table="meta:"+tablename of
099 * include table; value=empty</li>
100 * <li>4. Table-RS-timestamp map rowkey="trslm:"+backupRoot+table_name; value = map[RS-> last WAL
101 * timestamp]</li>
102 * <li>5. RS - WAL ts map rowkey="rslogts:"+backupRoot +server; value = last WAL timestamp</li>
103 * <li>6. WALs recorded rowkey="wals:"+WAL unique file name; value = backupId and full WAL file
104 * name</li>
105 * </ul>
106 * </p>
107 */
108@InterfaceAudience.Private
109public final class BackupSystemTable implements Closeable {
110
111  private static final Logger LOG = LoggerFactory.getLogger(BackupSystemTable.class);
112
113  static class WALItem {
114    String backupId;
115    String walFile;
116    String backupRoot;
117
118    WALItem(String backupId, String walFile, String backupRoot) {
119      this.backupId = backupId;
120      this.walFile = walFile;
121      this.backupRoot = backupRoot;
122    }
123
124    public String getBackupId() {
125      return backupId;
126    }
127
128    public String getWalFile() {
129      return walFile;
130    }
131
132    public String getBackupRoot() {
133      return backupRoot;
134    }
135
136    @Override
137    public String toString() {
138      return Path.SEPARATOR + backupRoot + Path.SEPARATOR + backupId + Path.SEPARATOR + walFile;
139    }
140  }
141
142  /**
143   * Backup system table (main) name
144   */
145  private TableName tableName;
146
147  /**
148   * Backup System table name for bulk loaded files. We keep all bulk loaded file references in a
149   * separate table because we have to isolate general backup operations: create, merge etc from
150   * activity of RegionObserver, which controls process of a bulk loading
151   * {@link org.apache.hadoop.hbase.backup.BackupObserver}
152   */
153  private TableName bulkLoadTableName;
154
155  /**
156   * Stores backup sessions (contexts)
157   */
158  final static byte[] SESSIONS_FAMILY = Bytes.toBytes("session");
159  /**
160   * Stores other meta
161   */
162  final static byte[] META_FAMILY = Bytes.toBytes("meta");
163  final static byte[] BULK_LOAD_FAMILY = Bytes.toBytes("bulk");
164  /**
165   * Connection to HBase cluster, shared among all instances
166   */
167  private final Connection connection;
168
169  private final static String BACKUP_INFO_PREFIX = "session:";
170  private final static String START_CODE_ROW = "startcode:";
171  private final static byte[] ACTIVE_SESSION_ROW = Bytes.toBytes("activesession:");
172  private final static byte[] ACTIVE_SESSION_COL = Bytes.toBytes("c");
173
174  private final static byte[] ACTIVE_SESSION_YES = Bytes.toBytes("yes");
175  private final static byte[] ACTIVE_SESSION_NO = Bytes.toBytes("no");
176
177  private final static String INCR_BACKUP_SET = "incrbackupset:";
178  private final static String CONTINUOUS_BACKUP_SET = "continuousbackupset";
179  /**
180   * Row key identifier for storing the last replicated WAL timestamp in the backup system table for
181   * continuous backup.
182   */
183  private static final String CONTINUOUS_BACKUP_REPLICATION_TIMESTAMP_ROW =
184    "continuous_backup_last_replicated";
185  private final static String TABLE_RS_LOG_MAP_PREFIX = "trslm:";
186  private final static String RS_LOG_TS_PREFIX = "rslogts:";
187
188  private final static String BULK_LOAD_PREFIX = "bulk:";
189  private final static byte[] BULK_LOAD_PREFIX_BYTES = Bytes.toBytes(BULK_LOAD_PREFIX);
190  private final static byte[] DELETE_OP_ROW = Bytes.toBytes("delete_op_row");
191  private final static byte[] MERGE_OP_ROW = Bytes.toBytes("merge_op_row");
192
193  final static byte[] TBL_COL = Bytes.toBytes("tbl");
194  final static byte[] FAM_COL = Bytes.toBytes("fam");
195  final static byte[] PATH_COL = Bytes.toBytes("path");
196
197  private final static String SET_KEY_PREFIX = "backupset:";
198
199  // separator between BULK_LOAD_PREFIX and ordinals
200  private final static String BLK_LD_DELIM = ":";
201  private final static byte[] EMPTY_VALUE = new byte[] {};
202
203  // Safe delimiter in a string
204  private final static String NULL = "\u0000";
205
206  public BackupSystemTable(Connection conn) throws IOException {
207    this.connection = conn;
208    Configuration conf = this.connection.getConfiguration();
209    tableName = BackupSystemTable.getTableName(conf);
210    bulkLoadTableName = BackupSystemTable.getTableNameForBulkLoadedData(conf);
211    checkSystemTable();
212  }
213
214  private void checkSystemTable() throws IOException {
215    try (Admin admin = connection.getAdmin()) {
216      verifyNamespaceExists(admin);
217      Configuration conf = connection.getConfiguration();
218      if (!admin.tableExists(tableName)) {
219        TableDescriptor backupHTD = BackupSystemTable.getSystemTableDescriptor(conf);
220        createSystemTable(admin, backupHTD);
221      }
222      ensureTableEnabled(admin, tableName);
223      if (!admin.tableExists(bulkLoadTableName)) {
224        TableDescriptor blHTD = BackupSystemTable.getSystemTableForBulkLoadedDataDescriptor(conf);
225        createSystemTable(admin, blHTD);
226      }
227      ensureTableEnabled(admin, bulkLoadTableName);
228      waitForSystemTable(admin, tableName);
229      waitForSystemTable(admin, bulkLoadTableName);
230    }
231  }
232
233  private void createSystemTable(Admin admin, TableDescriptor descriptor) throws IOException {
234    try {
235      admin.createTable(descriptor);
236    } catch (TableExistsException e) {
237      // swallow because this class is initialized in concurrent environments (i.e. bulkloads),
238      // so may be subject to race conditions where one caller succeeds in creating the
239      // table and others fail because it now exists
240      LOG.debug("Table {} already exists, ignoring", descriptor.getTableName(), e);
241    }
242  }
243
244  private void verifyNamespaceExists(Admin admin) throws IOException {
245    String namespaceName = tableName.getNamespaceAsString();
246    NamespaceDescriptor ns = NamespaceDescriptor.create(namespaceName).build();
247    NamespaceDescriptor[] list = admin.listNamespaceDescriptors();
248    boolean exists = false;
249    for (NamespaceDescriptor nsd : list) {
250      if (nsd.getName().equals(ns.getName())) {
251        exists = true;
252        break;
253      }
254    }
255    if (!exists) {
256      try {
257        admin.createNamespace(ns);
258      } catch (NamespaceExistException e) {
259        // swallow because this class is initialized in concurrent environments (i.e. bulkloads),
260        // so may be subject to race conditions where one caller succeeds in creating the
261        // namespace and others fail because it now exists
262        LOG.debug("Namespace {} already exists, ignoring", ns.getName(), e);
263      }
264    }
265  }
266
267  private void waitForSystemTable(Admin admin, TableName tableName) throws IOException {
268    // Return fast if the table is available and avoid a log message
269    if (admin.tableExists(tableName) && admin.isTableAvailable(tableName)) {
270      return;
271    }
272    long TIMEOUT = 60000;
273    long startTime = EnvironmentEdgeManager.currentTime();
274    LOG.debug("Backup table {} is not present and available, waiting for it to become so",
275      tableName);
276    while (!admin.tableExists(tableName) || !admin.isTableAvailable(tableName)) {
277      try {
278        Thread.sleep(100);
279      } catch (InterruptedException e) {
280        throw (IOException) new InterruptedIOException().initCause(e);
281      }
282      if (EnvironmentEdgeManager.currentTime() - startTime > TIMEOUT) {
283        throw new IOException(
284          "Failed to create backup system table " + tableName + " after " + TIMEOUT + "ms");
285      }
286    }
287    LOG.debug("Backup table {} exists and available", tableName);
288  }
289
290  @Override
291  public void close() {
292    // do nothing
293  }
294
295  /**
296   * Updates status (state) of a backup session in backup system table table
297   * @param info backup info
298   * @throws IOException exception
299   */
300  public void updateBackupInfo(BackupInfo info) throws IOException {
301    if (LOG.isTraceEnabled()) {
302      LOG.trace("update backup status in backup system table for: " + info.getBackupId()
303        + " set status=" + info.getState());
304    }
305    try (Table table = connection.getTable(tableName)) {
306      Put put = createPutForBackupInfo(info);
307      table.put(put);
308    }
309  }
310
311  /*
312   * @param backupId the backup Id
313   * @return Map of rows to path of bulk loaded hfile
314   */
315  Map<byte[], String> readBulkLoadedFiles(String backupId) throws IOException {
316    Scan scan = BackupSystemTable.createScanForBulkLoadedFiles(backupId);
317    try (Table table = connection.getTable(bulkLoadTableName);
318      ResultScanner scanner = table.getScanner(scan)) {
319      Result res = null;
320      Map<byte[], String> map = new TreeMap<>(Bytes.BYTES_COMPARATOR);
321      while ((res = scanner.next()) != null) {
322        res.advance();
323        byte[] row = CellUtil.cloneRow(res.listCells().get(0));
324        for (Cell cell : res.listCells()) {
325          if (
326            CellUtil.compareQualifiers(cell, BackupSystemTable.PATH_COL, 0,
327              BackupSystemTable.PATH_COL.length) == 0
328          ) {
329            map.put(row, Bytes.toString(CellUtil.cloneValue(cell)));
330          }
331        }
332      }
333      return map;
334    }
335  }
336
337  /**
338   * Deletes backup status from backup system table table
339   * @param backupId backup id
340   * @throws IOException exception
341   */
342  public void deleteBackupInfo(String backupId) throws IOException {
343    if (LOG.isTraceEnabled()) {
344      LOG.trace("delete backup status in backup system table for " + backupId);
345    }
346    try (Table table = connection.getTable(tableName)) {
347      Delete del = createDeleteForBackupInfo(backupId);
348      table.delete(del);
349    }
350  }
351
352  /**
353   * Registers a bulk load.
354   * @param tableName     table name
355   * @param region        the region receiving hfile
356   * @param cfToHfilePath column family and associated hfiles
357   */
358  public void registerBulkLoad(TableName tableName, byte[] region,
359    Map<byte[], List<Path>> cfToHfilePath) throws IOException {
360    if (LOG.isDebugEnabled()) {
361      LOG.debug("Writing bulk load descriptor to backup {} with {} entries", tableName,
362        cfToHfilePath.size());
363    }
364    try (BufferedMutator bufferedMutator = connection.getBufferedMutator(bulkLoadTableName)) {
365      List<Put> puts = BackupSystemTable.createPutForBulkLoad(tableName, region, cfToHfilePath);
366      bufferedMutator.mutate(puts);
367      LOG.debug("Written {} rows for bulk load of table {}", puts.size(), tableName);
368    }
369  }
370
371  /**
372   * Removes entries from the table that tracks all bulk loaded hfiles.
373   * @param rows the row keys of the entries to be deleted
374   */
375  public void deleteBulkLoadedRows(List<byte[]> rows) throws IOException {
376    try (BufferedMutator bufferedMutator = connection.getBufferedMutator(bulkLoadTableName)) {
377      List<Delete> deletes = new ArrayList<>();
378      for (byte[] row : rows) {
379        Delete del = new Delete(row);
380        deletes.add(del);
381        LOG.debug("Deleting bulk load entry with key: {}", Bytes.toString(row));
382      }
383      bufferedMutator.mutate(deletes);
384      LOG.debug("Deleted {} bulk load entries.", rows.size());
385    }
386  }
387
388  /**
389   * Reads the rows from backup table recording bulk loaded hfiles
390   */
391  public List<BulkLoad> readBulkloadRows() throws IOException {
392    Scan scan = BackupSystemTable.createScanForOrigBulkLoadedFiles(null);
393    return processBulkLoadRowScan(scan, Long.MAX_VALUE);
394  }
395
396  /**
397   * Reads the rows from backup table recording bulk loaded hfiles
398   * @param tableList list of table names
399   */
400  public List<BulkLoad> readBulkloadRows(Collection<TableName> tableList) throws IOException {
401    return readBulkloadRows(tableList, Long.MAX_VALUE);
402  }
403
404  /**
405   * Reads the rows from backup table recording bulk loaded hfiles
406   * @param tableList    list of table names
407   * @param endTimestamp upper bound timestamp for bulkload entries retrieval
408   */
409  public List<BulkLoad> readBulkloadRows(Collection<TableName> tableList, long endTimestamp)
410    throws IOException {
411    List<BulkLoad> result = new ArrayList<>();
412    for (TableName table : tableList) {
413      Scan scan = BackupSystemTable.createScanForOrigBulkLoadedFiles(table);
414      result.addAll(processBulkLoadRowScan(scan, endTimestamp));
415    }
416    return result;
417  }
418
419  private List<BulkLoad> processBulkLoadRowScan(Scan scan, long endTimestamp) throws IOException {
420    List<BulkLoad> result = new ArrayList<>();
421    try (Table bulkLoadTable = connection.getTable(bulkLoadTableName);
422      ResultScanner scanner = bulkLoadTable.getScanner(scan)) {
423      Result res;
424      while ((res = scanner.next()) != null) {
425        res.advance();
426        TableName table = null;
427        String fam = null;
428        String path = null;
429        String region = null;
430        byte[] row = null;
431        long timestamp = 0L;
432        for (Cell cell : res.listCells()) {
433          row = CellUtil.cloneRow(cell);
434          timestamp = cell.getTimestamp();
435          String rowStr = Bytes.toString(row);
436          region = BackupSystemTable.getRegionNameFromOrigBulkLoadRow(rowStr);
437          if (
438            CellUtil.compareQualifiers(cell, BackupSystemTable.TBL_COL, 0,
439              BackupSystemTable.TBL_COL.length) == 0
440          ) {
441            table = TableName.valueOf(CellUtil.cloneValue(cell));
442          } else if (
443            CellUtil.compareQualifiers(cell, BackupSystemTable.FAM_COL, 0,
444              BackupSystemTable.FAM_COL.length) == 0
445          ) {
446            fam = Bytes.toString(CellUtil.cloneValue(cell));
447          } else if (
448            CellUtil.compareQualifiers(cell, BackupSystemTable.PATH_COL, 0,
449              BackupSystemTable.PATH_COL.length) == 0
450          ) {
451            path = Bytes.toString(CellUtil.cloneValue(cell));
452          }
453        }
454        LOG.debug("Found orig path {} for family {} of table {} and region {} with timestamp {}",
455          path, fam, table, region, timestamp);
456        if (timestamp <= endTimestamp) {
457          result.add(new BulkLoad(table, region, fam, path, row, timestamp));
458        }
459      }
460    }
461    return result;
462  }
463
464  /**
465   * Reads backup status object (instance of backup info) from backup system table table
466   * @param backupId backup id
467   * @return Current status of backup session or null
468   */
469  public BackupInfo readBackupInfo(String backupId) throws IOException {
470    if (LOG.isTraceEnabled()) {
471      LOG.trace("read backup status from backup system table for: " + backupId);
472    }
473
474    try (Table table = connection.getTable(tableName)) {
475      Get get = createGetForBackupInfo(backupId);
476      Result res = table.get(get);
477      if (res.isEmpty()) {
478        return null;
479      }
480      return resultToBackupInfo(res);
481    }
482  }
483
484  /**
485   * Exclusive operations are: create, delete, merge
486   * @throws IOException if a table operation fails or an active backup exclusive operation is
487   *                     already underway
488   */
489  public void startBackupExclusiveOperation() throws IOException {
490    LOG.debug("Start new backup exclusive operation");
491
492    try (Table table = connection.getTable(tableName)) {
493      Put put = createPutForStartBackupSession();
494      // First try to put if row does not exist
495      if (
496        !table.checkAndMutate(ACTIVE_SESSION_ROW, SESSIONS_FAMILY).qualifier(ACTIVE_SESSION_COL)
497          .ifNotExists().thenPut(put)
498      ) {
499        // Row exists, try to put if value == ACTIVE_SESSION_NO
500        if (
501          !table.checkAndMutate(ACTIVE_SESSION_ROW, SESSIONS_FAMILY).qualifier(ACTIVE_SESSION_COL)
502            .ifEquals(ACTIVE_SESSION_NO).thenPut(put)
503        ) {
504          throw new ExclusiveOperationException();
505        }
506      }
507    }
508  }
509
510  private Put createPutForStartBackupSession() {
511    Put put = new Put(ACTIVE_SESSION_ROW);
512    put.addColumn(SESSIONS_FAMILY, ACTIVE_SESSION_COL, ACTIVE_SESSION_YES);
513    return put;
514  }
515
516  public void finishBackupExclusiveOperation() throws IOException {
517    LOG.debug("Finish backup exclusive operation");
518
519    try (Table table = connection.getTable(tableName)) {
520      Put put = createPutForStopBackupSession();
521      if (
522        !table.checkAndMutate(ACTIVE_SESSION_ROW, SESSIONS_FAMILY).qualifier(ACTIVE_SESSION_COL)
523          .ifEquals(ACTIVE_SESSION_YES).thenPut(put)
524      ) {
525        throw new IOException("There is no active backup exclusive operation");
526      }
527    }
528  }
529
530  private Put createPutForStopBackupSession() {
531    Put put = new Put(ACTIVE_SESSION_ROW);
532    put.addColumn(SESSIONS_FAMILY, ACTIVE_SESSION_COL, ACTIVE_SESSION_NO);
533    return put;
534  }
535
536  /**
537   * Get the Region Servers log information after the last log roll from backup system table.
538   * @param backupRoot root directory path to backup
539   * @return RS log info
540   * @throws IOException exception
541   */
542  public HashMap<String, Long> readRegionServerLastLogRollResult(String backupRoot)
543    throws IOException {
544    LOG.trace("read region server last roll log result to backup system table");
545
546    Scan scan = createScanForReadRegionServerLastLogRollResult(backupRoot);
547
548    try (Table table = connection.getTable(tableName);
549      ResultScanner scanner = table.getScanner(scan)) {
550      Result res;
551      HashMap<String, Long> rsTimestampMap = new HashMap<>();
552      while ((res = scanner.next()) != null) {
553        res.advance();
554        Cell cell = res.current();
555        byte[] row = CellUtil.cloneRow(cell);
556        String server = getServerNameForReadRegionServerLastLogRollResult(row);
557        byte[] data = CellUtil.cloneValue(cell);
558        rsTimestampMap.put(server, Bytes.toLong(data));
559      }
560      return rsTimestampMap;
561    }
562  }
563
564  /**
565   * Writes Region Server last roll log result (timestamp) to backup system table table
566   * @param server     Region Server name
567   * @param ts         last log timestamp
568   * @param backupRoot root directory path to backup
569   * @throws IOException exception
570   */
571  public void writeRegionServerLastLogRollResult(String server, Long ts, String backupRoot)
572    throws IOException {
573    LOG.trace("write region server last roll log result to backup system table");
574
575    try (Table table = connection.getTable(tableName)) {
576      Put put = createPutForRegionServerLastLogRollResult(server, ts, backupRoot);
577      table.put(put);
578    }
579  }
580
581  /**
582   * Retrieve all table names that are part of any known completed backup
583   */
584  public Set<TableName> getTablesIncludedInBackups() throws IOException {
585    // Incremental backups have the same tables as the preceding full backups
586    List<BackupInfo> infos =
587      getBackupHistory(withState(BackupState.COMPLETE), withType(BackupType.FULL));
588    return infos.stream().flatMap(info -> info.getTableNames().stream())
589      .collect(Collectors.toSet());
590  }
591
592  /**
593   * Goes through all backup history corresponding to the provided root folder, and collects all
594   * backup info mentioning each of the provided tables.
595   * @param set        the tables for which to collect the {@code BackupInfo}
596   * @param backupRoot backup destination path to retrieve backup history for
597   * @return a map containing (a subset of) the provided {@code TableName}s, mapped to a list of at
598   *         least one {@code BackupInfo}
599   * @throws IOException if getting the backup history fails
600   */
601  public Map<TableName, List<BackupInfo>> getBackupHistoryForTableSet(Set<TableName> set,
602    String backupRoot) throws IOException {
603    List<BackupInfo> history = getBackupHistory(withRoot(backupRoot));
604    Map<TableName, List<BackupInfo>> tableHistoryMap = new HashMap<>();
605    for (BackupInfo info : history) {
606      List<TableName> tables = info.getTableNames();
607      for (TableName tableName : tables) {
608        if (set.contains(tableName)) {
609          List<BackupInfo> list =
610            tableHistoryMap.computeIfAbsent(tableName, k -> new ArrayList<>());
611          list.add(info);
612        }
613      }
614    }
615    return tableHistoryMap;
616  }
617
618  /**
619   * Get all backup information passing the given filters, ordered by descending backupId. I.e. from
620   * newest to oldest.
621   */
622  public List<BackupInfo> getBackupHistory(BackupInfo.Filter... toInclude) throws IOException {
623    return getBackupHistory(Order.NEW_TO_OLD, Integer.MAX_VALUE, toInclude);
624  }
625
626  /**
627   * Retrieves the first n entries of the sorted, filtered list of backup infos.
628   * @param order desired ordering of the results.
629   * @param n     number of entries to return
630   */
631  public List<BackupInfo> getBackupHistory(Order order, int n, BackupInfo.Filter... toInclude)
632    throws IOException {
633    Preconditions.checkArgument(n >= 0, "n should be >= 0");
634    LOG.trace("get backup infos from backup system table");
635
636    if (n == 0) {
637      return Collections.emptyList();
638    }
639
640    Predicate<BackupInfo> combinedPredicate = Stream.of(toInclude)
641      .map(filter -> (Predicate<BackupInfo>) filter).reduce(Predicate::and).orElse(x -> true);
642
643    Scan scan = createScanForBackupHistory(order);
644    List<BackupInfo> list = new ArrayList<>();
645
646    try (Table table = connection.getTable(tableName);
647      ResultScanner scanner = table.getScanner(scan)) {
648      Result res;
649      while ((res = scanner.next()) != null) {
650        res.advance();
651        BackupInfo context = cellToBackupInfo(res.current());
652        if (combinedPredicate.test(context)) {
653          list.add(context);
654          if (list.size() == n) {
655            break;
656          }
657        }
658      }
659      return list;
660    }
661  }
662
663  /**
664   * Write the current timestamps for each regionserver to backup system table after a successful
665   * full or incremental backup. The saved timestamp is of the last log file that was backed up
666   * already.
667   * @param tables        tables
668   * @param newTimestamps timestamps
669   * @param backupRoot    root directory path to backup
670   * @throws IOException exception
671   */
672  public void writeRegionServerLogTimestamp(Set<TableName> tables, Map<String, Long> newTimestamps,
673    String backupRoot) throws IOException {
674    if (LOG.isTraceEnabled()) {
675      LOG.trace("write RS log time stamps to backup system table for tables ["
676        + StringUtils.join(tables, ",") + "]");
677    }
678    List<Put> puts = new ArrayList<>();
679    for (TableName table : tables) {
680      byte[] smapData = toTableServerTimestampProto(table, newTimestamps).toByteArray();
681      Put put = createPutForWriteRegionServerLogTimestamp(table, smapData, backupRoot);
682      puts.add(put);
683    }
684    try (BufferedMutator bufferedMutator = connection.getBufferedMutator(tableName)) {
685      bufferedMutator.mutate(puts);
686    }
687  }
688
689  /**
690   * Read the timestamp for each region server log after the last successful backup. Each table has
691   * its own set of the timestamps. The info is stored for each table as a concatenated string of
692   * rs->timestapmp
693   * @param backupRoot root directory path to backup
694   * @return the timestamp for each region server. key: tableName value:
695   *         RegionServer,PreviousTimeStamp
696   * @throws IOException exception
697   */
698  public Map<TableName, Map<String, Long>> readLogTimestampMap(String backupRoot)
699    throws IOException {
700    if (LOG.isTraceEnabled()) {
701      LOG.trace("read RS log ts from backup system table for root=" + backupRoot);
702    }
703
704    Map<TableName, Map<String, Long>> tableTimestampMap = new HashMap<>();
705
706    Scan scan = createScanForReadLogTimestampMap(backupRoot);
707    try (Table table = connection.getTable(tableName);
708      ResultScanner scanner = table.getScanner(scan)) {
709      Result res;
710      while ((res = scanner.next()) != null) {
711        res.advance();
712        Cell cell = res.current();
713        byte[] row = CellUtil.cloneRow(cell);
714        String tabName = getTableNameForReadLogTimestampMap(row);
715        TableName tn = TableName.valueOf(tabName);
716        byte[] data = CellUtil.cloneValue(cell);
717        if (data == null) {
718          throw new IOException("Data of last backup data from backup system table "
719            + "is empty. Create a backup first.");
720        }
721        if (data != null && data.length > 0) {
722          HashMap<String, Long> lastBackup =
723            fromTableServerTimestampProto(BackupProtos.TableServerTimestamp.parseFrom(data));
724          tableTimestampMap.put(tn, lastBackup);
725        }
726      }
727      return tableTimestampMap;
728    }
729  }
730
731  private BackupProtos.TableServerTimestamp toTableServerTimestampProto(TableName table,
732    Map<String, Long> map) {
733    BackupProtos.TableServerTimestamp.Builder tstBuilder =
734      BackupProtos.TableServerTimestamp.newBuilder();
735    tstBuilder
736      .setTableName(org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil.toProtoTableName(table));
737
738    for (Entry<String, Long> entry : map.entrySet()) {
739      BackupProtos.ServerTimestamp.Builder builder = BackupProtos.ServerTimestamp.newBuilder();
740      HBaseProtos.ServerName.Builder snBuilder = HBaseProtos.ServerName.newBuilder();
741      ServerName sn = ServerName.parseServerName(entry.getKey());
742      snBuilder.setHostName(sn.getHostname());
743      snBuilder.setPort(sn.getPort());
744      builder.setServerName(snBuilder.build());
745      builder.setTimestamp(entry.getValue());
746      tstBuilder.addServerTimestamp(builder.build());
747    }
748
749    return tstBuilder.build();
750  }
751
752  private HashMap<String, Long>
753    fromTableServerTimestampProto(BackupProtos.TableServerTimestamp proto) {
754
755    HashMap<String, Long> map = new HashMap<>();
756    List<BackupProtos.ServerTimestamp> list = proto.getServerTimestampList();
757    for (BackupProtos.ServerTimestamp st : list) {
758      ServerName sn =
759        org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil.toServerName(st.getServerName());
760      map.put(sn.getHostname() + ":" + sn.getPort(), st.getTimestamp());
761    }
762    return map;
763  }
764
765  /**
766   * Return the current tables covered by incremental backup.
767   * @param backupRoot root directory path to backup
768   * @return set of tableNames
769   * @throws IOException exception
770   */
771  public Set<TableName> getIncrementalBackupTableSet(String backupRoot) throws IOException {
772    LOG.trace("get incremental backup table set from backup system table");
773
774    TreeSet<TableName> set = new TreeSet<>();
775
776    try (Table table = connection.getTable(tableName)) {
777      Get get = createGetForIncrBackupTableSet(backupRoot);
778      Result res = table.get(get);
779      if (res.isEmpty()) {
780        return set;
781      }
782      List<Cell> cells = res.listCells();
783      for (Cell cell : cells) {
784        // qualifier = table name - we use table names as qualifiers
785        set.add(TableName.valueOf(CellUtil.cloneQualifier(cell)));
786      }
787      return set;
788    }
789  }
790
791  /**
792   * Retrieves the current set of tables covered by continuous backup along with the timestamp
793   * indicating when continuous backup started for each table.
794   * @return a map where the key is the table name and the value is the timestamp representing the
795   *         start time of continuous backup for that table.
796   * @throws IOException if an I/O error occurs while accessing the backup system table.
797   */
798  public Map<TableName, Long> getContinuousBackupTableSet() throws IOException {
799    LOG.trace("Retrieving continuous backup table set from the backup system table.");
800    Map<TableName, Long> tableMap = new TreeMap<>();
801
802    try (Table systemTable = connection.getTable(tableName)) {
803      Get getOperation = createGetForContinuousBackupTableSet();
804      Result result = systemTable.get(getOperation);
805
806      if (result.isEmpty()) {
807        return tableMap;
808      }
809
810      // Extract table names and timestamps from the result cells
811      List<Cell> cells = result.listCells();
812      for (Cell cell : cells) {
813        TableName tableName = TableName.valueOf(CellUtil.cloneQualifier(cell));
814        long timestamp = Bytes.toLong(CellUtil.cloneValue(cell));
815        tableMap.put(tableName, timestamp);
816      }
817    }
818
819    return tableMap;
820  }
821
822  /**
823   * Add tables to global incremental backup set
824   * @param tables     set of tables
825   * @param backupRoot root directory path to backup
826   * @throws IOException exception
827   */
828  public void addIncrementalBackupTableSet(Set<TableName> tables, String backupRoot)
829    throws IOException {
830    if (LOG.isTraceEnabled()) {
831      LOG.trace("Add incremental backup table set to backup system table. ROOT=" + backupRoot
832        + " tables [" + StringUtils.join(tables, " ") + "]");
833    }
834    if (LOG.isDebugEnabled()) {
835      tables.forEach(table -> LOG.debug(Objects.toString(table)));
836    }
837    try (Table table = connection.getTable(tableName)) {
838      Put put = createPutForIncrBackupTableSet(tables, backupRoot);
839      table.put(put);
840    }
841  }
842
843  /**
844   * Add tables to the global continuous backup set. Only updates tables that are not already in the
845   * continuous backup set.
846   * @param tables         set of tables to add
847   * @param startTimestamp timestamp indicating when continuous backup started
848   * @throws IOException if an error occurs while updating the backup system table
849   */
850  public void addContinuousBackupTableSet(Set<TableName> tables, long startTimestamp)
851    throws IOException {
852    if (LOG.isTraceEnabled()) {
853      LOG.trace("Add continuous backup table set to backup system table. tables ["
854        + StringUtils.join(tables, " ") + "]");
855    }
856    if (LOG.isDebugEnabled()) {
857      tables.forEach(table -> LOG.debug(Objects.toString(table)));
858    }
859
860    // Get existing continuous backup tables
861    Map<TableName, Long> existingTables = getContinuousBackupTableSet();
862
863    try (Table table = connection.getTable(tableName)) {
864      Put put = createPutForContinuousBackupTableSet(tables, existingTables, startTimestamp);
865      if (!put.isEmpty()) {
866        table.put(put);
867      }
868    }
869  }
870
871  /**
872   * Updates the system table with the new start timestamps for continuous backup tables.
873   * @param tablesToUpdate    The set of tables that need their start timestamps updated.
874   * @param newStartTimestamp The new start timestamp to be set.
875   */
876  public void updateContinuousBackupTableSet(Set<TableName> tablesToUpdate, long newStartTimestamp)
877    throws IOException {
878    if (tablesToUpdate == null || tablesToUpdate.isEmpty()) {
879      LOG.warn("No tables provided for updating start timestamps.");
880      return;
881    }
882
883    try (Table table = connection.getTable(tableName)) {
884      Put put = new Put(rowkey(CONTINUOUS_BACKUP_SET));
885
886      for (TableName tableName : tablesToUpdate) {
887        put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(tableName.getNameAsString()),
888          Bytes.toBytes(newStartTimestamp));
889      }
890
891      table.put(put);
892      LOG.info("Successfully updated start timestamps for {} tables in the backup system table.",
893        tablesToUpdate.size());
894    }
895  }
896
897  /**
898   * Removes tables from the global continuous backup set. Only removes entries that currently exist
899   * in the backup system table.
900   * @param tables set of tables to remove
901   * @throws IOException if an error occurs while updating the backup system table
902   */
903  public void removeContinuousBackupTableSet(Set<TableName> tables) throws IOException {
904    if (LOG.isTraceEnabled()) {
905      LOG.trace("Remove continuous backup table set from backup system table. tables ["
906        + StringUtils.join(tables, " ") + "]");
907    }
908    if (LOG.isDebugEnabled()) {
909      tables.forEach(table -> LOG.debug("Removing: " + table));
910    }
911
912    Map<TableName, Long> existingTables = getContinuousBackupTableSet();
913    Set<TableName> toRemove =
914      tables.stream().filter(existingTables::containsKey).collect(Collectors.toSet());
915
916    if (toRemove.isEmpty()) {
917      LOG.debug("No matching tables found to remove from continuous backup set.");
918      return;
919    }
920
921    try (Table table = connection.getTable(tableName)) {
922      Delete delete = createDeleteForContinuousBackupTableSet(toRemove);
923      table.delete(delete);
924    }
925  }
926
927  /**
928   * Updates the latest replicated WAL timestamp for a region server in the backup system table.
929   * This is used to track the replication checkpoint for continuous backup and PITR (Point-in-Time
930   * Restore).
931   * @param serverName the server for which the latest WAL timestamp is being recorded
932   * @param timestamp  the timestamp (in milliseconds) of the last WAL entry replicated
933   * @throws IOException if an error occurs while writing to the backup system table
934   */
935  public void updateBackupCheckpointTimestamp(ServerName serverName, long timestamp)
936    throws IOException {
937
938    HBaseProtos.ServerName.Builder serverProto =
939      HBaseProtos.ServerName.newBuilder().setHostName(serverName.getHostname())
940        .setPort(serverName.getPort()).setStartCode(serverName.getStartCode());
941
942    try (Table table = connection.getTable(tableName)) {
943      Put put = createPutForBackupCheckpoint(serverProto.build().toByteArray(), timestamp);
944      if (!put.isEmpty()) {
945        table.put(put);
946      }
947    }
948  }
949
950  /**
951   * Retrieves the latest replicated WAL timestamps for all region servers from the backup system
952   * table. This is used to track the replication checkpoint state for continuous backup and PITR
953   * (Point-in-Time Restore).
954   * @return a map where the key is {@link ServerName} and the value is the latest replicated WAL
955   *         timestamp in milliseconds
956   * @throws IOException if an error occurs while reading from the backup system table
957   */
958  public Map<ServerName, Long> getBackupCheckpointTimestamps() throws IOException {
959    LOG.trace("Fetching latest backup checkpoint timestamps for all region servers.");
960
961    Map<ServerName, Long> checkpointMap = new HashMap<>();
962
963    byte[] rowKey = rowkey(CONTINUOUS_BACKUP_REPLICATION_TIMESTAMP_ROW);
964    Get get = new Get(rowKey);
965    get.addFamily(BackupSystemTable.META_FAMILY);
966
967    try (Table table = connection.getTable(tableName)) {
968      Result result = table.get(get);
969
970      if (result.isEmpty()) {
971        LOG.debug("No checkpoint timestamps found in backup system table.");
972        return checkpointMap;
973      }
974
975      List<Cell> cells = result.listCells();
976      for (Cell cell : cells) {
977        try {
978          HBaseProtos.ServerName protoServer =
979            HBaseProtos.ServerName.parseFrom(CellUtil.cloneQualifier(cell));
980          ServerName serverName = ServerName.valueOf(protoServer.getHostName(),
981            protoServer.getPort(), protoServer.getStartCode());
982
983          long timestamp = Bytes.toLong(CellUtil.cloneValue(cell));
984          checkpointMap.put(serverName, timestamp);
985        } catch (IllegalArgumentException e) {
986          LOG.warn("Failed to parse server name or timestamp from cell: {}", cell, e);
987        }
988      }
989    }
990
991    return checkpointMap;
992  }
993
994  /**
995   * Constructs a {@link Put} operation to update the last replicated WAL timestamp for a given
996   * server in the backup system table.
997   * @param serverNameBytes the serialized server name as bytes
998   * @param timestamp       the WAL entry timestamp to store
999   * @return a {@link Put} object ready to be written to the system table
1000   */
1001  private Put createPutForBackupCheckpoint(byte[] serverNameBytes, long timestamp) {
1002    Put put = new Put(rowkey(CONTINUOUS_BACKUP_REPLICATION_TIMESTAMP_ROW));
1003    put.addColumn(BackupSystemTable.META_FAMILY, serverNameBytes, Bytes.toBytes(timestamp));
1004    return put;
1005  }
1006
1007  /**
1008   * Deletes incremental backup set for a backup destination
1009   * @param backupRoot backup root
1010   */
1011  public void deleteIncrementalBackupTableSet(String backupRoot) throws IOException {
1012    if (LOG.isTraceEnabled()) {
1013      LOG.trace("Delete incremental backup table set to backup system table. ROOT=" + backupRoot);
1014    }
1015    try (Table table = connection.getTable(tableName)) {
1016      Delete delete = createDeleteForIncrBackupTableSet(backupRoot);
1017      table.delete(delete);
1018    }
1019  }
1020
1021  /**
1022   * Checks if we have at least one backup session in backup system table This API is used by
1023   * BackupLogCleaner
1024   * @return true, if at least one session exists in backup system table
1025   * @throws IOException exception
1026   */
1027  public boolean hasBackupSessions() throws IOException {
1028    LOG.trace("Has backup sessions from backup system table");
1029
1030    Scan scan = createScanForBackupHistory(Order.OLD_TO_NEW);
1031    scan.setCaching(1);
1032    try (Table table = connection.getTable(tableName);
1033      ResultScanner scanner = table.getScanner(scan)) {
1034      return scanner.next() != null;
1035    }
1036  }
1037
1038  /**
1039   * BACKUP SETS
1040   */
1041
1042  /**
1043   * Get backup set list
1044   * @return backup set list
1045   * @throws IOException if a table or scanner operation fails
1046   */
1047  public List<String> listBackupSets() throws IOException {
1048    LOG.trace("Backup set list");
1049
1050    List<String> list = new ArrayList<>();
1051    try (Table table = connection.getTable(tableName)) {
1052      Scan scan = createScanForBackupSetList();
1053      scan.readVersions(1);
1054      try (ResultScanner scanner = table.getScanner(scan)) {
1055        Result res;
1056        while ((res = scanner.next()) != null) {
1057          res.advance();
1058          list.add(cellKeyToBackupSetName(res.current()));
1059        }
1060        return list;
1061      }
1062    }
1063  }
1064
1065  /**
1066   * Get backup set description (list of tables)
1067   * @param name set's name
1068   * @return list of tables in a backup set
1069   * @throws IOException if a table operation fails
1070   */
1071  public List<TableName> describeBackupSet(String name) throws IOException {
1072    if (LOG.isTraceEnabled()) {
1073      LOG.trace(" Backup set describe: " + name);
1074    }
1075    try (Table table = connection.getTable(tableName)) {
1076      Get get = createGetForBackupSet(name);
1077      Result res = table.get(get);
1078      if (res.isEmpty()) {
1079        return null;
1080      }
1081      res.advance();
1082      String[] tables = cellValueToBackupSet(res.current());
1083      return Arrays.asList(tables).stream().map(item -> TableName.valueOf(item))
1084        .collect(Collectors.toList());
1085    }
1086  }
1087
1088  /**
1089   * Add backup set (list of tables)
1090   * @param name      set name
1091   * @param newTables list of tables, comma-separated
1092   * @throws IOException if a table operation fails
1093   */
1094  public void addToBackupSet(String name, String[] newTables) throws IOException {
1095    if (LOG.isTraceEnabled()) {
1096      LOG.trace("Backup set add: " + name + " tables [" + StringUtils.join(newTables, " ") + "]");
1097    }
1098    String[] union = null;
1099    try (Table table = connection.getTable(tableName)) {
1100      Get get = createGetForBackupSet(name);
1101      Result res = table.get(get);
1102      if (res.isEmpty()) {
1103        union = newTables;
1104      } else {
1105        res.advance();
1106        String[] tables = cellValueToBackupSet(res.current());
1107        union = merge(tables, newTables);
1108      }
1109      Put put = createPutForBackupSet(name, union);
1110      table.put(put);
1111    }
1112  }
1113
1114  /**
1115   * Remove tables from backup set (list of tables)
1116   * @param name     set name
1117   * @param toRemove list of tables
1118   * @throws IOException if a table operation or deleting the backup set fails
1119   */
1120  public void removeFromBackupSet(String name, String[] toRemove) throws IOException {
1121    if (LOG.isTraceEnabled()) {
1122      LOG.trace(
1123        " Backup set remove from : " + name + " tables [" + StringUtils.join(toRemove, " ") + "]");
1124    }
1125    String[] disjoint;
1126    String[] tables;
1127    try (Table table = connection.getTable(tableName)) {
1128      Get get = createGetForBackupSet(name);
1129      Result res = table.get(get);
1130      if (res.isEmpty()) {
1131        LOG.warn("Backup set '" + name + "' not found.");
1132        return;
1133      } else {
1134        res.advance();
1135        tables = cellValueToBackupSet(res.current());
1136        disjoint = disjoin(tables, toRemove);
1137      }
1138      if (disjoint.length > 0 && disjoint.length != tables.length) {
1139        Put put = createPutForBackupSet(name, disjoint);
1140        table.put(put);
1141      } else if (disjoint.length == tables.length) {
1142        LOG.warn("Backup set '" + name + "' does not contain tables ["
1143          + StringUtils.join(toRemove, " ") + "]");
1144      } else { // disjoint.length == 0 and tables.length >0
1145        // Delete backup set
1146        LOG.info("Backup set '" + name + "' is empty. Deleting.");
1147        deleteBackupSet(name);
1148      }
1149    }
1150  }
1151
1152  private String[] merge(String[] existingTables, String[] newTables) {
1153    Set<String> tables = new HashSet<>(Arrays.asList(existingTables));
1154    tables.addAll(Arrays.asList(newTables));
1155    return tables.toArray(new String[0]);
1156  }
1157
1158  private String[] disjoin(String[] existingTables, String[] toRemove) {
1159    Set<String> tables = new HashSet<>(Arrays.asList(existingTables));
1160    Arrays.asList(toRemove).forEach(table -> tables.remove(table));
1161    return tables.toArray(new String[0]);
1162  }
1163
1164  /**
1165   * Delete backup set
1166   * @param name set's name
1167   * @throws IOException if getting or deleting the table fails
1168   */
1169  public void deleteBackupSet(String name) throws IOException {
1170    if (LOG.isTraceEnabled()) {
1171      LOG.trace(" Backup set delete: " + name);
1172    }
1173    try (Table table = connection.getTable(tableName)) {
1174      Delete del = createDeleteForBackupSet(name);
1175      table.delete(del);
1176    }
1177  }
1178
1179  /**
1180   * Get backup system table descriptor
1181   * @return table's descriptor
1182   */
1183  public static TableDescriptor getSystemTableDescriptor(Configuration conf) {
1184    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(getTableName(conf));
1185
1186    ColumnFamilyDescriptorBuilder colBuilder =
1187      ColumnFamilyDescriptorBuilder.newBuilder(SESSIONS_FAMILY);
1188
1189    colBuilder.setMaxVersions(1);
1190    Configuration config = HBaseConfiguration.create();
1191    int ttl = config.getInt(BackupRestoreConstants.BACKUP_SYSTEM_TTL_KEY,
1192      BackupRestoreConstants.BACKUP_SYSTEM_TTL_DEFAULT);
1193    colBuilder.setTimeToLive(ttl);
1194
1195    ColumnFamilyDescriptor colSessionsDesc = colBuilder.build();
1196    builder.setColumnFamily(colSessionsDesc);
1197
1198    colBuilder = ColumnFamilyDescriptorBuilder.newBuilder(META_FAMILY);
1199    colBuilder.setTimeToLive(ttl);
1200    builder.setColumnFamily(colBuilder.build());
1201    return builder.build();
1202  }
1203
1204  public static TableName getTableName(Configuration conf) {
1205    String name = conf.get(BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_KEY,
1206      BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_DEFAULT);
1207    return TableName.valueOf(name);
1208  }
1209
1210  public static String getTableNameAsString(Configuration conf) {
1211    return getTableName(conf).getNameAsString();
1212  }
1213
1214  public static String getSnapshotName(Configuration conf) {
1215    return "snapshot_" + getTableNameAsString(conf).replace(":", "_");
1216  }
1217
1218  /**
1219   * Get backup system table descriptor
1220   * @return table's descriptor
1221   */
1222  public static TableDescriptor getSystemTableForBulkLoadedDataDescriptor(Configuration conf) {
1223    TableDescriptorBuilder builder =
1224      TableDescriptorBuilder.newBuilder(getTableNameForBulkLoadedData(conf));
1225
1226    ColumnFamilyDescriptorBuilder colBuilder =
1227      ColumnFamilyDescriptorBuilder.newBuilder(SESSIONS_FAMILY);
1228    colBuilder.setMaxVersions(1);
1229    Configuration config = HBaseConfiguration.create();
1230    int ttl = config.getInt(BackupRestoreConstants.BACKUP_SYSTEM_TTL_KEY,
1231      BackupRestoreConstants.BACKUP_SYSTEM_TTL_DEFAULT);
1232    colBuilder.setTimeToLive(ttl);
1233    ColumnFamilyDescriptor colSessionsDesc = colBuilder.build();
1234    builder.setColumnFamily(colSessionsDesc);
1235    colBuilder = ColumnFamilyDescriptorBuilder.newBuilder(META_FAMILY);
1236    colBuilder.setTimeToLive(ttl);
1237    builder.setColumnFamily(colBuilder.build());
1238    return builder.build();
1239  }
1240
1241  public static TableName getTableNameForBulkLoadedData(Configuration conf) {
1242    String name = conf.get(BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_KEY,
1243      BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_DEFAULT) + "_bulk";
1244    return TableName.valueOf(name);
1245  }
1246
1247  /**
1248   * Creates Put operation for a given backup info object
1249   * @param context backup info
1250   * @return put operation
1251   * @throws IOException exception
1252   */
1253  private Put createPutForBackupInfo(BackupInfo context) throws IOException {
1254    Put put = new Put(rowkey(BACKUP_INFO_PREFIX, context.getBackupId()));
1255    put.addColumn(BackupSystemTable.SESSIONS_FAMILY, Bytes.toBytes("context"),
1256      context.toByteArray());
1257    return put;
1258  }
1259
1260  /**
1261   * Creates Get operation for a given backup id
1262   * @param backupId backup's ID
1263   * @return get operation
1264   * @throws IOException exception
1265   */
1266  private Get createGetForBackupInfo(String backupId) throws IOException {
1267    Get get = new Get(rowkey(BACKUP_INFO_PREFIX, backupId));
1268    get.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1269    get.readVersions(1);
1270    return get;
1271  }
1272
1273  /**
1274   * Creates Delete operation for a given backup id
1275   * @param backupId backup's ID
1276   * @return delete operation
1277   */
1278  private Delete createDeleteForBackupInfo(String backupId) {
1279    Delete del = new Delete(rowkey(BACKUP_INFO_PREFIX, backupId));
1280    del.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1281    return del;
1282  }
1283
1284  /**
1285   * Converts Result to BackupInfo
1286   * @param res HBase result
1287   * @return backup info instance
1288   * @throws IOException exception
1289   */
1290  private BackupInfo resultToBackupInfo(Result res) throws IOException {
1291    res.advance();
1292    Cell cell = res.current();
1293    return cellToBackupInfo(cell);
1294  }
1295
1296  /**
1297   * Creates Get to retrieve incremental backup table set from backup system table
1298   * @return get operation
1299   * @throws IOException exception
1300   */
1301  private Get createGetForIncrBackupTableSet(String backupRoot) throws IOException {
1302    Get get = new Get(rowkey(INCR_BACKUP_SET, backupRoot));
1303    get.addFamily(BackupSystemTable.META_FAMILY);
1304    get.readVersions(1);
1305    return get;
1306  }
1307
1308  /**
1309   * Creates a Get operation to retrieve the continuous backup table set from the backup system
1310   * table.
1311   * @return a Get operation for retrieving the table set
1312   */
1313  private Get createGetForContinuousBackupTableSet() throws IOException {
1314    Get get = new Get(rowkey(CONTINUOUS_BACKUP_SET));
1315    get.addFamily(BackupSystemTable.META_FAMILY);
1316    get.readVersions(1);
1317    return get;
1318  }
1319
1320  /**
1321   * Creates Put to store incremental backup table set
1322   * @param tables tables
1323   * @return put operation
1324   */
1325  private Put createPutForIncrBackupTableSet(Set<TableName> tables, String backupRoot) {
1326    Put put = new Put(rowkey(INCR_BACKUP_SET, backupRoot));
1327    for (TableName table : tables) {
1328      put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(table.getNameAsString()),
1329        EMPTY_VALUE);
1330    }
1331    return put;
1332  }
1333
1334  /**
1335   * Creates a Put operation to store the continuous backup table set. Only includes tables that are
1336   * not already in the set.
1337   * @param tables         tables to add
1338   * @param existingTables tables that already have continuous backup enabled
1339   * @param startTimestamp timestamp indicating when continuous backup started
1340   * @return put operation
1341   */
1342  private Put createPutForContinuousBackupTableSet(Set<TableName> tables,
1343    Map<TableName, Long> existingTables, long startTimestamp) {
1344    Put put = new Put(rowkey(CONTINUOUS_BACKUP_SET));
1345
1346    for (TableName table : tables) {
1347      if (!existingTables.containsKey(table)) {
1348        put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(table.getNameAsString()),
1349          Bytes.toBytes(startTimestamp));
1350      }
1351    }
1352
1353    return put;
1354  }
1355
1356  /**
1357   * Creates Delete for incremental backup table set
1358   * @param backupRoot backup root
1359   * @return delete operation
1360   */
1361  private Delete createDeleteForIncrBackupTableSet(String backupRoot) {
1362    Delete delete = new Delete(rowkey(INCR_BACKUP_SET, backupRoot));
1363    delete.addFamily(BackupSystemTable.META_FAMILY);
1364    return delete;
1365  }
1366
1367  /**
1368   * Creates Delete for continuous backup table set
1369   * @param tables tables to remove
1370   * @return delete operation
1371   */
1372  private Delete createDeleteForContinuousBackupTableSet(Set<TableName> tables) {
1373    Delete delete = new Delete(rowkey(CONTINUOUS_BACKUP_SET));
1374    for (TableName tableName : tables) {
1375      delete.addColumn(META_FAMILY, Bytes.toBytes(tableName.getNameAsString()));
1376    }
1377    return delete;
1378  }
1379
1380  /**
1381   * Creates Scan operation to load backup history
1382   * @param order order of the scan results
1383   * @return scan operation
1384   */
1385  private Scan createScanForBackupHistory(Order order) {
1386    Scan scan = new Scan();
1387    byte[] startRow = Bytes.toBytes(BACKUP_INFO_PREFIX);
1388    if (order == Order.NEW_TO_OLD) {
1389      byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1390      stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1391      scan.setReversed(true);
1392      scan.withStartRow(stopRow, false);
1393      scan.withStopRow(startRow);
1394    } else if (order == Order.OLD_TO_NEW) {
1395      scan.setStartStopRowForPrefixScan(startRow);
1396    } else {
1397      throw new IllegalArgumentException("Unsupported order: " + order);
1398    }
1399    scan.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1400    scan.readVersions(1);
1401    return scan;
1402  }
1403
1404  /**
1405   * Converts cell to backup info instance.
1406   * @param current current cell
1407   * @return backup backup info instance
1408   * @throws IOException exception
1409   */
1410  private BackupInfo cellToBackupInfo(Cell current) throws IOException {
1411    byte[] data = CellUtil.cloneValue(current);
1412    return BackupInfo.fromByteArray(data);
1413  }
1414
1415  /**
1416   * Creates Put to write RS last roll log timestamp map
1417   * @param table table
1418   * @param smap  map, containing RS:ts
1419   * @return put operation
1420   */
1421  private Put createPutForWriteRegionServerLogTimestamp(TableName table, byte[] smap,
1422    String backupRoot) {
1423    Put put = new Put(rowkey(TABLE_RS_LOG_MAP_PREFIX, backupRoot, NULL, table.getNameAsString()));
1424    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("log-roll-map"), smap);
1425    return put;
1426  }
1427
1428  /**
1429   * Creates Scan to load table-> { RS -> ts} map of maps
1430   * @return scan operation
1431   */
1432  private Scan createScanForReadLogTimestampMap(String backupRoot) {
1433    Scan scan = new Scan();
1434    scan.setStartStopRowForPrefixScan(rowkey(TABLE_RS_LOG_MAP_PREFIX, backupRoot, NULL));
1435    scan.addFamily(BackupSystemTable.META_FAMILY);
1436
1437    return scan;
1438  }
1439
1440  /**
1441   * Get table name from rowkey
1442   * @param cloneRow rowkey
1443   * @return table name
1444   */
1445  private String getTableNameForReadLogTimestampMap(byte[] cloneRow) {
1446    String s = Bytes.toString(cloneRow);
1447    int index = s.lastIndexOf(NULL);
1448    return s.substring(index + 1);
1449  }
1450
1451  /**
1452   * Creates Put to store RS last log result
1453   * @param server    server name
1454   * @param timestamp log roll result (timestamp)
1455   * @return put operation
1456   */
1457  private Put createPutForRegionServerLastLogRollResult(String server, Long timestamp,
1458    String backupRoot) {
1459    Put put = new Put(rowkey(RS_LOG_TS_PREFIX, backupRoot, NULL, server));
1460    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("rs-log-ts"),
1461      Bytes.toBytes(timestamp));
1462    return put;
1463  }
1464
1465  /**
1466   * Creates Scan operation to load last RS log roll results
1467   * @return scan operation
1468   */
1469  private Scan createScanForReadRegionServerLastLogRollResult(String backupRoot) {
1470    Scan scan = new Scan();
1471    scan.setStartStopRowForPrefixScan(rowkey(RS_LOG_TS_PREFIX, backupRoot, NULL));
1472    scan.addFamily(BackupSystemTable.META_FAMILY);
1473    scan.readVersions(1);
1474
1475    return scan;
1476  }
1477
1478  /**
1479   * Get server's name from rowkey
1480   * @param row rowkey
1481   * @return server's name
1482   */
1483  private String getServerNameForReadRegionServerLastLogRollResult(byte[] row) {
1484    String s = Bytes.toString(row);
1485    int index = s.lastIndexOf(NULL);
1486    return s.substring(index + 1);
1487  }
1488
1489  /**
1490   * Creates Put's for bulk loads.
1491   */
1492  private static List<Put> createPutForBulkLoad(TableName table, byte[] region,
1493    Map<byte[], List<Path>> columnFamilyToHFilePaths) {
1494    List<Put> puts = new ArrayList<>();
1495    for (Map.Entry<byte[], List<Path>> entry : columnFamilyToHFilePaths.entrySet()) {
1496      for (Path path : entry.getValue()) {
1497        String file = path.toString();
1498        int lastSlash = file.lastIndexOf("/");
1499        String filename = file.substring(lastSlash + 1);
1500        Put put = new Put(rowkey(BULK_LOAD_PREFIX, table.toString(), BLK_LD_DELIM,
1501          Bytes.toString(region), BLK_LD_DELIM, filename));
1502        put.addColumn(BackupSystemTable.META_FAMILY, TBL_COL, table.getName());
1503        put.addColumn(BackupSystemTable.META_FAMILY, FAM_COL, entry.getKey());
1504        put.addColumn(BackupSystemTable.META_FAMILY, PATH_COL, Bytes.toBytes(file));
1505        puts.add(put);
1506        LOG.debug("Done writing bulk path {} for {} {}", file, table, Bytes.toString(region));
1507      }
1508    }
1509    return puts;
1510  }
1511
1512  public static void snapshot(Connection conn) throws IOException {
1513    try (Admin admin = conn.getAdmin()) {
1514      Configuration conf = conn.getConfiguration();
1515      admin.snapshot(BackupSystemTable.getSnapshotName(conf), BackupSystemTable.getTableName(conf));
1516    }
1517  }
1518
1519  public static void restoreFromSnapshot(Connection conn) throws IOException {
1520    Configuration conf = conn.getConfiguration();
1521    LOG.debug("Restoring " + BackupSystemTable.getTableNameAsString(conf) + " from snapshot");
1522    try (Admin admin = conn.getAdmin()) {
1523      String snapshotName = BackupSystemTable.getSnapshotName(conf);
1524      if (snapshotExists(admin, snapshotName)) {
1525        admin.restoreBackupSystemTable(snapshotName);
1526        LOG.debug("Done restoring backup system table");
1527      } else {
1528        // Snapshot does not exists, i.e completeBackup failed after
1529        // deleting backup system table snapshot
1530        // In this case we log WARN and proceed
1531        LOG.warn(
1532          "Could not restore backup system table. Snapshot " + snapshotName + " does not exists.");
1533      }
1534    }
1535  }
1536
1537  private static boolean snapshotExists(Admin admin, String snapshotName) throws IOException {
1538    List<SnapshotDescription> list = admin.listSnapshots();
1539    for (SnapshotDescription desc : list) {
1540      if (desc.getName().equals(snapshotName)) {
1541        return true;
1542      }
1543    }
1544    return false;
1545  }
1546
1547  public static boolean snapshotExists(Connection conn) throws IOException {
1548    return snapshotExists(conn.getAdmin(), getSnapshotName(conn.getConfiguration()));
1549  }
1550
1551  public static void deleteSnapshot(Connection conn) throws IOException {
1552    Configuration conf = conn.getConfiguration();
1553    LOG.debug("Deleting " + BackupSystemTable.getSnapshotName(conf) + " from the system");
1554    try (Admin admin = conn.getAdmin()) {
1555      String snapshotName = BackupSystemTable.getSnapshotName(conf);
1556      if (snapshotExists(admin, snapshotName)) {
1557        admin.deleteSnapshot(snapshotName);
1558        LOG.debug("Done deleting backup system table snapshot");
1559      } else {
1560        LOG.error("Snapshot " + snapshotName + " does not exists");
1561      }
1562    }
1563  }
1564
1565  private Put createPutForDeleteOperation(String[] backupIdList) {
1566    byte[] value = Bytes.toBytes(StringUtils.join(backupIdList, ","));
1567    Put put = new Put(DELETE_OP_ROW);
1568    put.addColumn(META_FAMILY, FAM_COL, value);
1569    return put;
1570  }
1571
1572  private Delete createDeleteForBackupDeleteOperation() {
1573    Delete delete = new Delete(DELETE_OP_ROW);
1574    delete.addFamily(META_FAMILY);
1575    return delete;
1576  }
1577
1578  private Get createGetForDeleteOperation() {
1579    Get get = new Get(DELETE_OP_ROW);
1580    get.addFamily(META_FAMILY);
1581    return get;
1582  }
1583
1584  public void startDeleteOperation(String[] backupIdList) throws IOException {
1585    if (LOG.isTraceEnabled()) {
1586      LOG.trace("Start delete operation for backups: " + StringUtils.join(backupIdList));
1587    }
1588    Put put = createPutForDeleteOperation(backupIdList);
1589    try (Table table = connection.getTable(tableName)) {
1590      table.put(put);
1591    }
1592  }
1593
1594  public void finishDeleteOperation() throws IOException {
1595    LOG.trace("Finsih delete operation for backup ids");
1596
1597    Delete delete = createDeleteForBackupDeleteOperation();
1598    try (Table table = connection.getTable(tableName)) {
1599      table.delete(delete);
1600    }
1601  }
1602
1603  public String[] getListOfBackupIdsFromDeleteOperation() throws IOException {
1604    LOG.trace("Get delete operation for backup ids");
1605
1606    Get get = createGetForDeleteOperation();
1607    try (Table table = connection.getTable(tableName)) {
1608      Result res = table.get(get);
1609      if (res.isEmpty()) {
1610        return null;
1611      }
1612      Cell cell = res.listCells().get(0);
1613      byte[] val = CellUtil.cloneValue(cell);
1614      if (val.length == 0) {
1615        return null;
1616      }
1617      return Splitter.on(',').splitToStream(new String(val, StandardCharsets.UTF_8))
1618        .toArray(String[]::new);
1619    }
1620  }
1621
1622  private Put createPutForMergeOperation(String[] backupIdList) {
1623    byte[] value = Bytes.toBytes(StringUtils.join(backupIdList, ","));
1624    Put put = new Put(MERGE_OP_ROW);
1625    put.addColumn(META_FAMILY, FAM_COL, value);
1626    return put;
1627  }
1628
1629  public boolean isMergeInProgress() throws IOException {
1630    Get get = new Get(MERGE_OP_ROW);
1631    try (Table table = connection.getTable(tableName)) {
1632      Result res = table.get(get);
1633      return !res.isEmpty();
1634    }
1635  }
1636
1637  private Put createPutForUpdateTablesForMerge(List<TableName> tables) {
1638    byte[] value = Bytes.toBytes(StringUtils.join(tables, ","));
1639    Put put = new Put(MERGE_OP_ROW);
1640    put.addColumn(META_FAMILY, PATH_COL, value);
1641    return put;
1642  }
1643
1644  private Delete createDeleteForBackupMergeOperation() {
1645    Delete delete = new Delete(MERGE_OP_ROW);
1646    delete.addFamily(META_FAMILY);
1647    return delete;
1648  }
1649
1650  private Get createGetForMergeOperation() {
1651    Get get = new Get(MERGE_OP_ROW);
1652    get.addFamily(META_FAMILY);
1653    return get;
1654  }
1655
1656  public void startMergeOperation(String[] backupIdList) throws IOException {
1657    if (LOG.isTraceEnabled()) {
1658      LOG.trace("Start merge operation for backups: " + StringUtils.join(backupIdList));
1659    }
1660    Put put = createPutForMergeOperation(backupIdList);
1661    try (Table table = connection.getTable(tableName)) {
1662      table.put(put);
1663    }
1664  }
1665
1666  public void updateProcessedTablesForMerge(List<TableName> tables) throws IOException {
1667    if (LOG.isTraceEnabled()) {
1668      LOG.trace("Update tables for merge : " + StringUtils.join(tables, ","));
1669    }
1670    Put put = createPutForUpdateTablesForMerge(tables);
1671    try (Table table = connection.getTable(tableName)) {
1672      table.put(put);
1673    }
1674  }
1675
1676  public void finishMergeOperation() throws IOException {
1677    LOG.trace("Finish merge operation for backup ids");
1678
1679    Delete delete = createDeleteForBackupMergeOperation();
1680    try (Table table = connection.getTable(tableName)) {
1681      table.delete(delete);
1682    }
1683  }
1684
1685  public String[] getListOfBackupIdsFromMergeOperation() throws IOException {
1686    LOG.trace("Get backup ids for merge operation");
1687
1688    Get get = createGetForMergeOperation();
1689    try (Table table = connection.getTable(tableName)) {
1690      Result res = table.get(get);
1691      if (res.isEmpty()) {
1692        return null;
1693      }
1694      Cell cell = res.listCells().get(0);
1695      byte[] val = CellUtil.cloneValue(cell);
1696      if (val.length == 0) {
1697        return null;
1698      }
1699      return Splitter.on(',').splitToStream(new String(val, StandardCharsets.UTF_8))
1700        .toArray(String[]::new);
1701    }
1702  }
1703
1704  /**
1705   * Creates a scan to read all registered bulk loads for the given table, or for all tables if
1706   * {@code table} is {@code null}.
1707   */
1708  static Scan createScanForOrigBulkLoadedFiles(@Nullable TableName table) {
1709    Scan scan = new Scan();
1710    byte[] startRow = table == null
1711      ? BULK_LOAD_PREFIX_BYTES
1712      : rowkey(BULK_LOAD_PREFIX, table.toString(), BLK_LD_DELIM);
1713    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1714    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1715    scan.withStartRow(startRow);
1716    scan.withStopRow(stopRow);
1717    scan.addFamily(BackupSystemTable.META_FAMILY);
1718    scan.readVersions(1);
1719    return scan;
1720  }
1721
1722  static String getTableNameFromOrigBulkLoadRow(String rowStr) {
1723    // format is bulk : namespace : table : region : file
1724    return Iterators.get(Splitter.onPattern(BLK_LD_DELIM).split(rowStr).iterator(), 1);
1725  }
1726
1727  static String getRegionNameFromOrigBulkLoadRow(String rowStr) {
1728    // format is bulk : namespace : table : region : file
1729    List<String> parts = Splitter.onPattern(BLK_LD_DELIM).splitToList(rowStr);
1730    Iterator<String> i = parts.iterator();
1731    int idx = 3;
1732    if (parts.size() == 4) {
1733      // the table is in default namespace
1734      idx = 2;
1735    }
1736    String region = Iterators.get(i, idx);
1737    LOG.debug("bulk row string " + rowStr + " region " + region);
1738    return region;
1739  }
1740
1741  /*
1742   * Used to query bulk loaded hfiles which have been copied by incremental backup
1743   * @param backupId the backup Id. It can be null when querying for all tables
1744   * @return the Scan object
1745   * @deprecated This method is broken if a backupId is specified - see HBASE-28715
1746   */
1747  static Scan createScanForBulkLoadedFiles(String backupId) {
1748    Scan scan = new Scan();
1749    byte[] startRow =
1750      backupId == null ? BULK_LOAD_PREFIX_BYTES : rowkey(BULK_LOAD_PREFIX, backupId + BLK_LD_DELIM);
1751    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1752    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1753    scan.withStartRow(startRow);
1754    scan.withStopRow(stopRow);
1755    scan.addFamily(BackupSystemTable.META_FAMILY);
1756    scan.readVersions(1);
1757    return scan;
1758  }
1759
1760  /**
1761   * Creates Scan operation to load backup set list
1762   * @return scan operation
1763   */
1764  private Scan createScanForBackupSetList() {
1765    Scan scan = new Scan();
1766    byte[] startRow = Bytes.toBytes(SET_KEY_PREFIX);
1767    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1768    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1769    scan.withStartRow(startRow);
1770    scan.withStopRow(stopRow);
1771    scan.addFamily(BackupSystemTable.META_FAMILY);
1772    return scan;
1773  }
1774
1775  /**
1776   * Creates Get operation to load backup set content
1777   * @return get operation
1778   */
1779  private Get createGetForBackupSet(String name) {
1780    Get get = new Get(rowkey(SET_KEY_PREFIX, name));
1781    get.addFamily(BackupSystemTable.META_FAMILY);
1782    return get;
1783  }
1784
1785  /**
1786   * Creates Delete operation to delete backup set content
1787   * @param name backup set's name
1788   * @return delete operation
1789   */
1790  private Delete createDeleteForBackupSet(String name) {
1791    Delete del = new Delete(rowkey(SET_KEY_PREFIX, name));
1792    del.addFamily(BackupSystemTable.META_FAMILY);
1793    return del;
1794  }
1795
1796  /**
1797   * Creates Put operation to update backup set content
1798   * @param name   backup set's name
1799   * @param tables list of tables
1800   * @return put operation
1801   */
1802  private Put createPutForBackupSet(String name, String[] tables) {
1803    Put put = new Put(rowkey(SET_KEY_PREFIX, name));
1804    byte[] value = convertToByteArray(tables);
1805    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("tables"), value);
1806    return put;
1807  }
1808
1809  private byte[] convertToByteArray(String[] tables) {
1810    return Bytes.toBytes(StringUtils.join(tables, ","));
1811  }
1812
1813  /**
1814   * Converts cell to backup set list.
1815   * @param current current cell
1816   * @return backup set as array of table names
1817   */
1818  private String[] cellValueToBackupSet(Cell current) {
1819    byte[] data = CellUtil.cloneValue(current);
1820    if (!ArrayUtils.isEmpty(data)) {
1821      return Bytes.toString(data).split(",");
1822    }
1823    return new String[0];
1824  }
1825
1826  /**
1827   * Converts cell key to backup set name.
1828   * @param current current cell
1829   * @return backup set name
1830   */
1831  private String cellKeyToBackupSetName(Cell current) {
1832    byte[] data = CellUtil.cloneRow(current);
1833    return Bytes.toString(data).substring(SET_KEY_PREFIX.length());
1834  }
1835
1836  private static byte[] rowkey(String s, String... other) {
1837    StringBuilder sb = new StringBuilder(s);
1838    for (String ss : other) {
1839      sb.append(ss);
1840    }
1841    return Bytes.toBytes(sb.toString());
1842  }
1843
1844  private static void ensureTableEnabled(Admin admin, TableName tableName) throws IOException {
1845    if (!admin.isTableEnabled(tableName)) {
1846      try {
1847        admin.enableTable(tableName);
1848      } catch (TableNotDisabledException ignored) {
1849        LOG.info("Table {} is not disabled, ignoring enable request", tableName);
1850      }
1851    }
1852  }
1853
1854  public enum Order {
1855    /**
1856     * Old backups first, most recents last. I.e. sorted by ascending backupId.
1857     */
1858    OLD_TO_NEW,
1859    /**
1860     * New backups first, oldest last. I.e. sorted by descending backupId.
1861     */
1862    NEW_TO_OLD
1863  }
1864}