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. For a region server that took part in the backup's log roll, the
666   * saved timestamp is the result of that roll. For a region server that did not (offline or
667   * decommissioned), it is the creation time of the newest of its log files that was backed up.
668   * @param tables        tables
669   * @param newTimestamps timestamps
670   * @param backupRoot    root directory path to backup
671   * @throws IOException exception
672   */
673  public void writeRegionServerLogTimestamp(Set<TableName> tables, Map<String, Long> newTimestamps,
674    String backupRoot) throws IOException {
675    if (LOG.isTraceEnabled()) {
676      LOG.trace("write RS log time stamps to backup system table for tables ["
677        + StringUtils.join(tables, ",") + "]");
678    }
679    List<Put> puts = new ArrayList<>();
680    for (TableName table : tables) {
681      byte[] smapData = toTableServerTimestampProto(table, newTimestamps).toByteArray();
682      Put put = createPutForWriteRegionServerLogTimestamp(table, smapData, backupRoot);
683      puts.add(put);
684    }
685    try (BufferedMutator bufferedMutator = connection.getBufferedMutator(tableName)) {
686      bufferedMutator.mutate(puts);
687    }
688  }
689
690  /**
691   * Read the timestamp for each region server log after the last successful backup. Each table has
692   * its own set of the timestamps. The info is stored for each table as a concatenated string of
693   * rs->timestapmp
694   * @param backupRoot root directory path to backup
695   * @return the timestamp for each region server. key: tableName value:
696   *         RegionServer,PreviousTimeStamp
697   * @throws IOException exception
698   */
699  public Map<TableName, Map<String, Long>> readLogTimestampMap(String backupRoot)
700    throws IOException {
701    if (LOG.isTraceEnabled()) {
702      LOG.trace("read RS log ts from backup system table for root=" + backupRoot);
703    }
704
705    Map<TableName, Map<String, Long>> tableTimestampMap = new HashMap<>();
706
707    Scan scan = createScanForReadLogTimestampMap(backupRoot);
708    try (Table table = connection.getTable(tableName);
709      ResultScanner scanner = table.getScanner(scan)) {
710      Result res;
711      while ((res = scanner.next()) != null) {
712        res.advance();
713        Cell cell = res.current();
714        byte[] row = CellUtil.cloneRow(cell);
715        String tabName = getTableNameForReadLogTimestampMap(row);
716        TableName tn = TableName.valueOf(tabName);
717        byte[] data = CellUtil.cloneValue(cell);
718        if (data == null) {
719          throw new IOException("Data of last backup data from backup system table "
720            + "is empty. Create a backup first.");
721        }
722        if (data != null && data.length > 0) {
723          HashMap<String, Long> lastBackup =
724            fromTableServerTimestampProto(BackupProtos.TableServerTimestamp.parseFrom(data));
725          tableTimestampMap.put(tn, lastBackup);
726        }
727      }
728      return tableTimestampMap;
729    }
730  }
731
732  private BackupProtos.TableServerTimestamp toTableServerTimestampProto(TableName table,
733    Map<String, Long> map) {
734    BackupProtos.TableServerTimestamp.Builder tstBuilder =
735      BackupProtos.TableServerTimestamp.newBuilder();
736    tstBuilder
737      .setTableName(org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil.toProtoTableName(table));
738
739    for (Entry<String, Long> entry : map.entrySet()) {
740      BackupProtos.ServerTimestamp.Builder builder = BackupProtos.ServerTimestamp.newBuilder();
741      HBaseProtos.ServerName.Builder snBuilder = HBaseProtos.ServerName.newBuilder();
742      ServerName sn = ServerName.parseServerName(entry.getKey());
743      snBuilder.setHostName(sn.getHostname());
744      snBuilder.setPort(sn.getPort());
745      builder.setServerName(snBuilder.build());
746      builder.setTimestamp(entry.getValue());
747      tstBuilder.addServerTimestamp(builder.build());
748    }
749
750    return tstBuilder.build();
751  }
752
753  private HashMap<String, Long>
754    fromTableServerTimestampProto(BackupProtos.TableServerTimestamp proto) {
755
756    HashMap<String, Long> map = new HashMap<>();
757    List<BackupProtos.ServerTimestamp> list = proto.getServerTimestampList();
758    for (BackupProtos.ServerTimestamp st : list) {
759      ServerName sn =
760        org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil.toServerName(st.getServerName());
761      map.put(sn.getHostname() + ":" + sn.getPort(), st.getTimestamp());
762    }
763    return map;
764  }
765
766  /**
767   * Return the current tables covered by incremental backup.
768   * @param backupRoot root directory path to backup
769   * @return set of tableNames
770   * @throws IOException exception
771   */
772  public Set<TableName> getIncrementalBackupTableSet(String backupRoot) throws IOException {
773    LOG.trace("get incremental backup table set from backup system table");
774
775    TreeSet<TableName> set = new TreeSet<>();
776
777    try (Table table = connection.getTable(tableName)) {
778      Get get = createGetForIncrBackupTableSet(backupRoot);
779      Result res = table.get(get);
780      if (res.isEmpty()) {
781        return set;
782      }
783      List<Cell> cells = res.listCells();
784      for (Cell cell : cells) {
785        // qualifier = table name - we use table names as qualifiers
786        set.add(TableName.valueOf(CellUtil.cloneQualifier(cell)));
787      }
788      return set;
789    }
790  }
791
792  /**
793   * Retrieves the current set of tables covered by continuous backup along with the timestamp
794   * indicating when continuous backup started for each table.
795   * @return a map where the key is the table name and the value is the timestamp representing the
796   *         start time of continuous backup for that table.
797   * @throws IOException if an I/O error occurs while accessing the backup system table.
798   */
799  public Map<TableName, Long> getContinuousBackupTableSet() throws IOException {
800    LOG.trace("Retrieving continuous backup table set from the backup system table.");
801    Map<TableName, Long> tableMap = new TreeMap<>();
802
803    try (Table systemTable = connection.getTable(tableName)) {
804      Get getOperation = createGetForContinuousBackupTableSet();
805      Result result = systemTable.get(getOperation);
806
807      if (result.isEmpty()) {
808        return tableMap;
809      }
810
811      // Extract table names and timestamps from the result cells
812      List<Cell> cells = result.listCells();
813      for (Cell cell : cells) {
814        TableName tableName = TableName.valueOf(CellUtil.cloneQualifier(cell));
815        long timestamp = Bytes.toLong(CellUtil.cloneValue(cell));
816        tableMap.put(tableName, timestamp);
817      }
818    }
819
820    return tableMap;
821  }
822
823  /**
824   * Add tables to global incremental backup set
825   * @param tables     set of tables
826   * @param backupRoot root directory path to backup
827   * @throws IOException exception
828   */
829  public void addIncrementalBackupTableSet(Set<TableName> tables, String backupRoot)
830    throws IOException {
831    if (LOG.isTraceEnabled()) {
832      LOG.trace("Add incremental backup table set to backup system table. ROOT=" + backupRoot
833        + " tables [" + StringUtils.join(tables, " ") + "]");
834    }
835    if (LOG.isDebugEnabled()) {
836      tables.forEach(table -> LOG.debug(Objects.toString(table)));
837    }
838    try (Table table = connection.getTable(tableName)) {
839      Put put = createPutForIncrBackupTableSet(tables, backupRoot);
840      table.put(put);
841    }
842  }
843
844  /**
845   * Add tables to the global continuous backup set. Only updates tables that are not already in the
846   * continuous backup set.
847   * @param tables         set of tables to add
848   * @param startTimestamp timestamp indicating when continuous backup started
849   * @throws IOException if an error occurs while updating the backup system table
850   */
851  public void addContinuousBackupTableSet(Set<TableName> tables, long startTimestamp)
852    throws IOException {
853    if (LOG.isTraceEnabled()) {
854      LOG.trace("Add continuous backup table set to backup system table. tables ["
855        + StringUtils.join(tables, " ") + "]");
856    }
857    if (LOG.isDebugEnabled()) {
858      tables.forEach(table -> LOG.debug(Objects.toString(table)));
859    }
860
861    // Get existing continuous backup tables
862    Map<TableName, Long> existingTables = getContinuousBackupTableSet();
863
864    try (Table table = connection.getTable(tableName)) {
865      Put put = createPutForContinuousBackupTableSet(tables, existingTables, startTimestamp);
866      if (!put.isEmpty()) {
867        table.put(put);
868      }
869    }
870  }
871
872  /**
873   * Updates the system table with the new start timestamps for continuous backup tables.
874   * @param tablesToUpdate    The set of tables that need their start timestamps updated.
875   * @param newStartTimestamp The new start timestamp to be set.
876   */
877  public void updateContinuousBackupTableSet(Set<TableName> tablesToUpdate, long newStartTimestamp)
878    throws IOException {
879    if (tablesToUpdate == null || tablesToUpdate.isEmpty()) {
880      LOG.warn("No tables provided for updating start timestamps.");
881      return;
882    }
883
884    try (Table table = connection.getTable(tableName)) {
885      Put put = new Put(rowkey(CONTINUOUS_BACKUP_SET));
886
887      for (TableName tableName : tablesToUpdate) {
888        put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(tableName.getNameAsString()),
889          Bytes.toBytes(newStartTimestamp));
890      }
891
892      table.put(put);
893      LOG.info("Successfully updated start timestamps for {} tables in the backup system table.",
894        tablesToUpdate.size());
895    }
896  }
897
898  /**
899   * Removes tables from the global continuous backup set. Only removes entries that currently exist
900   * in the backup system table.
901   * @param tables set of tables to remove
902   * @throws IOException if an error occurs while updating the backup system table
903   */
904  public void removeContinuousBackupTableSet(Set<TableName> tables) throws IOException {
905    if (LOG.isTraceEnabled()) {
906      LOG.trace("Remove continuous backup table set from backup system table. tables ["
907        + StringUtils.join(tables, " ") + "]");
908    }
909    if (LOG.isDebugEnabled()) {
910      tables.forEach(table -> LOG.debug("Removing: " + table));
911    }
912
913    Map<TableName, Long> existingTables = getContinuousBackupTableSet();
914    Set<TableName> toRemove =
915      tables.stream().filter(existingTables::containsKey).collect(Collectors.toSet());
916
917    if (toRemove.isEmpty()) {
918      LOG.debug("No matching tables found to remove from continuous backup set.");
919      return;
920    }
921
922    try (Table table = connection.getTable(tableName)) {
923      Delete delete = createDeleteForContinuousBackupTableSet(toRemove);
924      table.delete(delete);
925    }
926  }
927
928  /**
929   * Updates the latest replicated WAL timestamp for a region server in the backup system table.
930   * This is used to track the replication checkpoint for continuous backup and PITR (Point-in-Time
931   * Restore).
932   * @param serverName the server for which the latest WAL timestamp is being recorded
933   * @param timestamp  the timestamp (in milliseconds) of the last WAL entry replicated
934   * @throws IOException if an error occurs while writing to the backup system table
935   */
936  public void updateBackupCheckpointTimestamp(ServerName serverName, long timestamp)
937    throws IOException {
938
939    HBaseProtos.ServerName.Builder serverProto =
940      HBaseProtos.ServerName.newBuilder().setHostName(serverName.getHostname())
941        .setPort(serverName.getPort()).setStartCode(serverName.getStartCode());
942
943    try (Table table = connection.getTable(tableName)) {
944      Put put = createPutForBackupCheckpoint(serverProto.build().toByteArray(), timestamp);
945      if (!put.isEmpty()) {
946        table.put(put);
947      }
948    }
949  }
950
951  /**
952   * Retrieves the latest replicated WAL timestamps for all region servers from the backup system
953   * table. This is used to track the replication checkpoint state for continuous backup and PITR
954   * (Point-in-Time Restore).
955   * @return a map where the key is {@link ServerName} and the value is the latest replicated WAL
956   *         timestamp in milliseconds
957   * @throws IOException if an error occurs while reading from the backup system table
958   */
959  public Map<ServerName, Long> getBackupCheckpointTimestamps() throws IOException {
960    LOG.trace("Fetching latest backup checkpoint timestamps for all region servers.");
961
962    Map<ServerName, Long> checkpointMap = new HashMap<>();
963
964    byte[] rowKey = rowkey(CONTINUOUS_BACKUP_REPLICATION_TIMESTAMP_ROW);
965    Get get = new Get(rowKey);
966    get.addFamily(BackupSystemTable.META_FAMILY);
967
968    try (Table table = connection.getTable(tableName)) {
969      Result result = table.get(get);
970
971      if (result.isEmpty()) {
972        LOG.debug("No checkpoint timestamps found in backup system table.");
973        return checkpointMap;
974      }
975
976      List<Cell> cells = result.listCells();
977      for (Cell cell : cells) {
978        try {
979          HBaseProtos.ServerName protoServer =
980            HBaseProtos.ServerName.parseFrom(CellUtil.cloneQualifier(cell));
981          ServerName serverName = ServerName.valueOf(protoServer.getHostName(),
982            protoServer.getPort(), protoServer.getStartCode());
983
984          long timestamp = Bytes.toLong(CellUtil.cloneValue(cell));
985          checkpointMap.put(serverName, timestamp);
986        } catch (IllegalArgumentException e) {
987          LOG.warn("Failed to parse server name or timestamp from cell: {}", cell, e);
988        }
989      }
990    }
991
992    return checkpointMap;
993  }
994
995  /**
996   * Constructs a {@link Put} operation to update the last replicated WAL timestamp for a given
997   * server in the backup system table.
998   * @param serverNameBytes the serialized server name as bytes
999   * @param timestamp       the WAL entry timestamp to store
1000   * @return a {@link Put} object ready to be written to the system table
1001   */
1002  private Put createPutForBackupCheckpoint(byte[] serverNameBytes, long timestamp) {
1003    Put put = new Put(rowkey(CONTINUOUS_BACKUP_REPLICATION_TIMESTAMP_ROW));
1004    put.addColumn(BackupSystemTable.META_FAMILY, serverNameBytes, Bytes.toBytes(timestamp));
1005    return put;
1006  }
1007
1008  /**
1009   * Deletes incremental backup set for a backup destination
1010   * @param backupRoot backup root
1011   */
1012  public void deleteIncrementalBackupTableSet(String backupRoot) throws IOException {
1013    if (LOG.isTraceEnabled()) {
1014      LOG.trace("Delete incremental backup table set to backup system table. ROOT=" + backupRoot);
1015    }
1016    try (Table table = connection.getTable(tableName)) {
1017      Delete delete = createDeleteForIncrBackupTableSet(backupRoot);
1018      table.delete(delete);
1019    }
1020  }
1021
1022  /**
1023   * Checks if we have at least one backup session in backup system table This API is used by
1024   * BackupLogCleaner
1025   * @return true, if at least one session exists in backup system table
1026   * @throws IOException exception
1027   */
1028  public boolean hasBackupSessions() throws IOException {
1029    LOG.trace("Has backup sessions from backup system table");
1030
1031    Scan scan = createScanForBackupHistory(Order.OLD_TO_NEW);
1032    scan.setCaching(1);
1033    try (Table table = connection.getTable(tableName);
1034      ResultScanner scanner = table.getScanner(scan)) {
1035      return scanner.next() != null;
1036    }
1037  }
1038
1039  /**
1040   * BACKUP SETS
1041   */
1042
1043  /**
1044   * Get backup set list
1045   * @return backup set list
1046   * @throws IOException if a table or scanner operation fails
1047   */
1048  public List<String> listBackupSets() throws IOException {
1049    LOG.trace("Backup set list");
1050
1051    List<String> list = new ArrayList<>();
1052    try (Table table = connection.getTable(tableName)) {
1053      Scan scan = createScanForBackupSetList();
1054      scan.readVersions(1);
1055      try (ResultScanner scanner = table.getScanner(scan)) {
1056        Result res;
1057        while ((res = scanner.next()) != null) {
1058          res.advance();
1059          list.add(cellKeyToBackupSetName(res.current()));
1060        }
1061        return list;
1062      }
1063    }
1064  }
1065
1066  /**
1067   * Get backup set description (list of tables)
1068   * @param name set's name
1069   * @return list of tables in a backup set
1070   * @throws IOException if a table operation fails
1071   */
1072  public List<TableName> describeBackupSet(String name) throws IOException {
1073    if (LOG.isTraceEnabled()) {
1074      LOG.trace(" Backup set describe: " + name);
1075    }
1076    try (Table table = connection.getTable(tableName)) {
1077      Get get = createGetForBackupSet(name);
1078      Result res = table.get(get);
1079      if (res.isEmpty()) {
1080        return null;
1081      }
1082      res.advance();
1083      String[] tables = cellValueToBackupSet(res.current());
1084      return Arrays.asList(tables).stream().map(item -> TableName.valueOf(item))
1085        .collect(Collectors.toList());
1086    }
1087  }
1088
1089  /**
1090   * Add backup set (list of tables)
1091   * @param name      set name
1092   * @param newTables list of tables, comma-separated
1093   * @throws IOException if a table operation fails
1094   */
1095  public void addToBackupSet(String name, String[] newTables) throws IOException {
1096    if (LOG.isTraceEnabled()) {
1097      LOG.trace("Backup set add: " + name + " tables [" + StringUtils.join(newTables, " ") + "]");
1098    }
1099    String[] union = null;
1100    try (Table table = connection.getTable(tableName)) {
1101      Get get = createGetForBackupSet(name);
1102      Result res = table.get(get);
1103      if (res.isEmpty()) {
1104        union = newTables;
1105      } else {
1106        res.advance();
1107        String[] tables = cellValueToBackupSet(res.current());
1108        union = merge(tables, newTables);
1109      }
1110      Put put = createPutForBackupSet(name, union);
1111      table.put(put);
1112    }
1113  }
1114
1115  /**
1116   * Remove tables from backup set (list of tables)
1117   * @param name     set name
1118   * @param toRemove list of tables
1119   * @throws IOException if a table operation or deleting the backup set fails
1120   */
1121  public void removeFromBackupSet(String name, String[] toRemove) throws IOException {
1122    if (LOG.isTraceEnabled()) {
1123      LOG.trace(
1124        " Backup set remove from : " + name + " tables [" + StringUtils.join(toRemove, " ") + "]");
1125    }
1126    String[] disjoint;
1127    String[] tables;
1128    try (Table table = connection.getTable(tableName)) {
1129      Get get = createGetForBackupSet(name);
1130      Result res = table.get(get);
1131      if (res.isEmpty()) {
1132        LOG.warn("Backup set '" + name + "' not found.");
1133        return;
1134      } else {
1135        res.advance();
1136        tables = cellValueToBackupSet(res.current());
1137        disjoint = disjoin(tables, toRemove);
1138      }
1139      if (disjoint.length > 0 && disjoint.length != tables.length) {
1140        Put put = createPutForBackupSet(name, disjoint);
1141        table.put(put);
1142      } else if (disjoint.length == tables.length) {
1143        LOG.warn("Backup set '" + name + "' does not contain tables ["
1144          + StringUtils.join(toRemove, " ") + "]");
1145      } else { // disjoint.length == 0 and tables.length >0
1146        // Delete backup set
1147        LOG.info("Backup set '" + name + "' is empty. Deleting.");
1148        deleteBackupSet(name);
1149      }
1150    }
1151  }
1152
1153  private String[] merge(String[] existingTables, String[] newTables) {
1154    Set<String> tables = new HashSet<>(Arrays.asList(existingTables));
1155    tables.addAll(Arrays.asList(newTables));
1156    return tables.toArray(new String[0]);
1157  }
1158
1159  private String[] disjoin(String[] existingTables, String[] toRemove) {
1160    Set<String> tables = new HashSet<>(Arrays.asList(existingTables));
1161    Arrays.asList(toRemove).forEach(table -> tables.remove(table));
1162    return tables.toArray(new String[0]);
1163  }
1164
1165  /**
1166   * Delete backup set
1167   * @param name set's name
1168   * @throws IOException if getting or deleting the table fails
1169   */
1170  public void deleteBackupSet(String name) throws IOException {
1171    if (LOG.isTraceEnabled()) {
1172      LOG.trace(" Backup set delete: " + name);
1173    }
1174    try (Table table = connection.getTable(tableName)) {
1175      Delete del = createDeleteForBackupSet(name);
1176      table.delete(del);
1177    }
1178  }
1179
1180  /**
1181   * Get backup system table descriptor
1182   * @return table's descriptor
1183   */
1184  public static TableDescriptor getSystemTableDescriptor(Configuration conf) {
1185    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(getTableName(conf));
1186
1187    ColumnFamilyDescriptorBuilder colBuilder =
1188      ColumnFamilyDescriptorBuilder.newBuilder(SESSIONS_FAMILY);
1189
1190    colBuilder.setMaxVersions(1);
1191    Configuration config = HBaseConfiguration.create();
1192    int ttl = config.getInt(BackupRestoreConstants.BACKUP_SYSTEM_TTL_KEY,
1193      BackupRestoreConstants.BACKUP_SYSTEM_TTL_DEFAULT);
1194    colBuilder.setTimeToLive(ttl);
1195
1196    ColumnFamilyDescriptor colSessionsDesc = colBuilder.build();
1197    builder.setColumnFamily(colSessionsDesc);
1198
1199    colBuilder = ColumnFamilyDescriptorBuilder.newBuilder(META_FAMILY);
1200    colBuilder.setTimeToLive(ttl);
1201    builder.setColumnFamily(colBuilder.build());
1202    return builder.build();
1203  }
1204
1205  public static TableName getTableName(Configuration conf) {
1206    String name = conf.get(BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_KEY,
1207      BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_DEFAULT);
1208    return TableName.valueOf(name);
1209  }
1210
1211  public static String getTableNameAsString(Configuration conf) {
1212    return getTableName(conf).getNameAsString();
1213  }
1214
1215  public static String getSnapshotName(Configuration conf) {
1216    return "snapshot_" + getTableNameAsString(conf).replace(":", "_");
1217  }
1218
1219  /**
1220   * Get backup system table descriptor
1221   * @return table's descriptor
1222   */
1223  public static TableDescriptor getSystemTableForBulkLoadedDataDescriptor(Configuration conf) {
1224    TableDescriptorBuilder builder =
1225      TableDescriptorBuilder.newBuilder(getTableNameForBulkLoadedData(conf));
1226
1227    ColumnFamilyDescriptorBuilder colBuilder =
1228      ColumnFamilyDescriptorBuilder.newBuilder(SESSIONS_FAMILY);
1229    colBuilder.setMaxVersions(1);
1230    Configuration config = HBaseConfiguration.create();
1231    int ttl = config.getInt(BackupRestoreConstants.BACKUP_SYSTEM_TTL_KEY,
1232      BackupRestoreConstants.BACKUP_SYSTEM_TTL_DEFAULT);
1233    colBuilder.setTimeToLive(ttl);
1234    ColumnFamilyDescriptor colSessionsDesc = colBuilder.build();
1235    builder.setColumnFamily(colSessionsDesc);
1236    colBuilder = ColumnFamilyDescriptorBuilder.newBuilder(META_FAMILY);
1237    colBuilder.setTimeToLive(ttl);
1238    builder.setColumnFamily(colBuilder.build());
1239    return builder.build();
1240  }
1241
1242  public static TableName getTableNameForBulkLoadedData(Configuration conf) {
1243    String name = conf.get(BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_KEY,
1244      BackupRestoreConstants.BACKUP_SYSTEM_TABLE_NAME_DEFAULT) + "_bulk";
1245    return TableName.valueOf(name);
1246  }
1247
1248  /**
1249   * Creates Put operation for a given backup info object
1250   * @param context backup info
1251   * @return put operation
1252   * @throws IOException exception
1253   */
1254  private Put createPutForBackupInfo(BackupInfo context) throws IOException {
1255    Put put = new Put(rowkey(BACKUP_INFO_PREFIX, context.getBackupId()));
1256    put.addColumn(BackupSystemTable.SESSIONS_FAMILY, Bytes.toBytes("context"),
1257      context.toByteArray());
1258    return put;
1259  }
1260
1261  /**
1262   * Creates Get operation for a given backup id
1263   * @param backupId backup's ID
1264   * @return get operation
1265   * @throws IOException exception
1266   */
1267  private Get createGetForBackupInfo(String backupId) throws IOException {
1268    Get get = new Get(rowkey(BACKUP_INFO_PREFIX, backupId));
1269    get.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1270    get.readVersions(1);
1271    return get;
1272  }
1273
1274  /**
1275   * Creates Delete operation for a given backup id
1276   * @param backupId backup's ID
1277   * @return delete operation
1278   */
1279  private Delete createDeleteForBackupInfo(String backupId) {
1280    Delete del = new Delete(rowkey(BACKUP_INFO_PREFIX, backupId));
1281    del.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1282    return del;
1283  }
1284
1285  /**
1286   * Converts Result to BackupInfo
1287   * @param res HBase result
1288   * @return backup info instance
1289   * @throws IOException exception
1290   */
1291  private BackupInfo resultToBackupInfo(Result res) throws IOException {
1292    res.advance();
1293    Cell cell = res.current();
1294    return cellToBackupInfo(cell);
1295  }
1296
1297  /**
1298   * Creates Get to retrieve incremental backup table set from backup system table
1299   * @return get operation
1300   * @throws IOException exception
1301   */
1302  private Get createGetForIncrBackupTableSet(String backupRoot) throws IOException {
1303    Get get = new Get(rowkey(INCR_BACKUP_SET, backupRoot));
1304    get.addFamily(BackupSystemTable.META_FAMILY);
1305    get.readVersions(1);
1306    return get;
1307  }
1308
1309  /**
1310   * Creates a Get operation to retrieve the continuous backup table set from the backup system
1311   * table.
1312   * @return a Get operation for retrieving the table set
1313   */
1314  private Get createGetForContinuousBackupTableSet() throws IOException {
1315    Get get = new Get(rowkey(CONTINUOUS_BACKUP_SET));
1316    get.addFamily(BackupSystemTable.META_FAMILY);
1317    get.readVersions(1);
1318    return get;
1319  }
1320
1321  /**
1322   * Creates Put to store incremental backup table set
1323   * @param tables tables
1324   * @return put operation
1325   */
1326  private Put createPutForIncrBackupTableSet(Set<TableName> tables, String backupRoot) {
1327    Put put = new Put(rowkey(INCR_BACKUP_SET, backupRoot));
1328    for (TableName table : tables) {
1329      put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(table.getNameAsString()),
1330        EMPTY_VALUE);
1331    }
1332    return put;
1333  }
1334
1335  /**
1336   * Creates a Put operation to store the continuous backup table set. Only includes tables that are
1337   * not already in the set.
1338   * @param tables         tables to add
1339   * @param existingTables tables that already have continuous backup enabled
1340   * @param startTimestamp timestamp indicating when continuous backup started
1341   * @return put operation
1342   */
1343  private Put createPutForContinuousBackupTableSet(Set<TableName> tables,
1344    Map<TableName, Long> existingTables, long startTimestamp) {
1345    Put put = new Put(rowkey(CONTINUOUS_BACKUP_SET));
1346
1347    for (TableName table : tables) {
1348      if (!existingTables.containsKey(table)) {
1349        put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes(table.getNameAsString()),
1350          Bytes.toBytes(startTimestamp));
1351      }
1352    }
1353
1354    return put;
1355  }
1356
1357  /**
1358   * Creates Delete for incremental backup table set
1359   * @param backupRoot backup root
1360   * @return delete operation
1361   */
1362  private Delete createDeleteForIncrBackupTableSet(String backupRoot) {
1363    Delete delete = new Delete(rowkey(INCR_BACKUP_SET, backupRoot));
1364    delete.addFamily(BackupSystemTable.META_FAMILY);
1365    return delete;
1366  }
1367
1368  /**
1369   * Creates Delete for continuous backup table set
1370   * @param tables tables to remove
1371   * @return delete operation
1372   */
1373  private Delete createDeleteForContinuousBackupTableSet(Set<TableName> tables) {
1374    Delete delete = new Delete(rowkey(CONTINUOUS_BACKUP_SET));
1375    for (TableName tableName : tables) {
1376      delete.addColumn(META_FAMILY, Bytes.toBytes(tableName.getNameAsString()));
1377    }
1378    return delete;
1379  }
1380
1381  /**
1382   * Creates Scan operation to load backup history
1383   * @param order order of the scan results
1384   * @return scan operation
1385   */
1386  private Scan createScanForBackupHistory(Order order) {
1387    Scan scan = new Scan();
1388    byte[] startRow = Bytes.toBytes(BACKUP_INFO_PREFIX);
1389    if (order == Order.NEW_TO_OLD) {
1390      byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1391      stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1392      scan.setReversed(true);
1393      scan.withStartRow(stopRow, false);
1394      scan.withStopRow(startRow);
1395    } else if (order == Order.OLD_TO_NEW) {
1396      scan.setStartStopRowForPrefixScan(startRow);
1397    } else {
1398      throw new IllegalArgumentException("Unsupported order: " + order);
1399    }
1400    scan.addFamily(BackupSystemTable.SESSIONS_FAMILY);
1401    scan.readVersions(1);
1402    return scan;
1403  }
1404
1405  /**
1406   * Converts cell to backup info instance.
1407   * @param current current cell
1408   * @return backup backup info instance
1409   * @throws IOException exception
1410   */
1411  private BackupInfo cellToBackupInfo(Cell current) throws IOException {
1412    byte[] data = CellUtil.cloneValue(current);
1413    return BackupInfo.fromByteArray(data);
1414  }
1415
1416  /**
1417   * Creates Put to write RS last roll log timestamp map
1418   * @param table table
1419   * @param smap  map, containing RS:ts
1420   * @return put operation
1421   */
1422  private Put createPutForWriteRegionServerLogTimestamp(TableName table, byte[] smap,
1423    String backupRoot) {
1424    Put put = new Put(rowkey(TABLE_RS_LOG_MAP_PREFIX, backupRoot, NULL, table.getNameAsString()));
1425    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("log-roll-map"), smap);
1426    return put;
1427  }
1428
1429  /**
1430   * Creates Scan to load table-> { RS -> ts} map of maps
1431   * @return scan operation
1432   */
1433  private Scan createScanForReadLogTimestampMap(String backupRoot) {
1434    Scan scan = new Scan();
1435    scan.setStartStopRowForPrefixScan(rowkey(TABLE_RS_LOG_MAP_PREFIX, backupRoot, NULL));
1436    scan.addFamily(BackupSystemTable.META_FAMILY);
1437
1438    return scan;
1439  }
1440
1441  /**
1442   * Get table name from rowkey
1443   * @param cloneRow rowkey
1444   * @return table name
1445   */
1446  private String getTableNameForReadLogTimestampMap(byte[] cloneRow) {
1447    String s = Bytes.toString(cloneRow);
1448    int index = s.lastIndexOf(NULL);
1449    return s.substring(index + 1);
1450  }
1451
1452  /**
1453   * Creates Put to store RS last log result
1454   * @param server    server name
1455   * @param timestamp log roll result (timestamp)
1456   * @return put operation
1457   */
1458  private Put createPutForRegionServerLastLogRollResult(String server, Long timestamp,
1459    String backupRoot) {
1460    Put put = new Put(rowkey(RS_LOG_TS_PREFIX, backupRoot, NULL, server));
1461    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("rs-log-ts"),
1462      Bytes.toBytes(timestamp));
1463    return put;
1464  }
1465
1466  /**
1467   * Creates Scan operation to load last RS log roll results
1468   * @return scan operation
1469   */
1470  private Scan createScanForReadRegionServerLastLogRollResult(String backupRoot) {
1471    Scan scan = new Scan();
1472    scan.setStartStopRowForPrefixScan(rowkey(RS_LOG_TS_PREFIX, backupRoot, NULL));
1473    scan.addFamily(BackupSystemTable.META_FAMILY);
1474    scan.readVersions(1);
1475
1476    return scan;
1477  }
1478
1479  /**
1480   * Get server's name from rowkey
1481   * @param row rowkey
1482   * @return server's name
1483   */
1484  private String getServerNameForReadRegionServerLastLogRollResult(byte[] row) {
1485    String s = Bytes.toString(row);
1486    int index = s.lastIndexOf(NULL);
1487    return s.substring(index + 1);
1488  }
1489
1490  /**
1491   * Creates Put's for bulk loads.
1492   */
1493  private static List<Put> createPutForBulkLoad(TableName table, byte[] region,
1494    Map<byte[], List<Path>> columnFamilyToHFilePaths) {
1495    List<Put> puts = new ArrayList<>();
1496    for (Map.Entry<byte[], List<Path>> entry : columnFamilyToHFilePaths.entrySet()) {
1497      for (Path path : entry.getValue()) {
1498        String file = path.toString();
1499        int lastSlash = file.lastIndexOf("/");
1500        String filename = file.substring(lastSlash + 1);
1501        Put put = new Put(rowkey(BULK_LOAD_PREFIX, table.toString(), BLK_LD_DELIM,
1502          Bytes.toString(region), BLK_LD_DELIM, filename));
1503        put.addColumn(BackupSystemTable.META_FAMILY, TBL_COL, table.getName());
1504        put.addColumn(BackupSystemTable.META_FAMILY, FAM_COL, entry.getKey());
1505        put.addColumn(BackupSystemTable.META_FAMILY, PATH_COL, Bytes.toBytes(file));
1506        puts.add(put);
1507        LOG.debug("Done writing bulk path {} for {} {}", file, table, Bytes.toString(region));
1508      }
1509    }
1510    return puts;
1511  }
1512
1513  public static void snapshot(Connection conn) throws IOException {
1514    try (Admin admin = conn.getAdmin()) {
1515      Configuration conf = conn.getConfiguration();
1516      admin.snapshot(BackupSystemTable.getSnapshotName(conf), BackupSystemTable.getTableName(conf));
1517    }
1518  }
1519
1520  public static void restoreFromSnapshot(Connection conn) throws IOException {
1521    Configuration conf = conn.getConfiguration();
1522    LOG.debug("Restoring " + BackupSystemTable.getTableNameAsString(conf) + " from snapshot");
1523    try (Admin admin = conn.getAdmin()) {
1524      String snapshotName = BackupSystemTable.getSnapshotName(conf);
1525      if (snapshotExists(admin, snapshotName)) {
1526        admin.restoreBackupSystemTable(snapshotName);
1527        LOG.debug("Done restoring backup system table");
1528      } else {
1529        // Snapshot does not exists, i.e completeBackup failed after
1530        // deleting backup system table snapshot
1531        // In this case we log WARN and proceed
1532        LOG.warn(
1533          "Could not restore backup system table. Snapshot " + snapshotName + " does not exists.");
1534      }
1535    }
1536  }
1537
1538  private static boolean snapshotExists(Admin admin, String snapshotName) throws IOException {
1539    List<SnapshotDescription> list = admin.listSnapshots();
1540    for (SnapshotDescription desc : list) {
1541      if (desc.getName().equals(snapshotName)) {
1542        return true;
1543      }
1544    }
1545    return false;
1546  }
1547
1548  public static boolean snapshotExists(Connection conn) throws IOException {
1549    return snapshotExists(conn.getAdmin(), getSnapshotName(conn.getConfiguration()));
1550  }
1551
1552  public static void deleteSnapshot(Connection conn) throws IOException {
1553    Configuration conf = conn.getConfiguration();
1554    LOG.debug("Deleting " + BackupSystemTable.getSnapshotName(conf) + " from the system");
1555    try (Admin admin = conn.getAdmin()) {
1556      String snapshotName = BackupSystemTable.getSnapshotName(conf);
1557      if (snapshotExists(admin, snapshotName)) {
1558        admin.deleteSnapshot(snapshotName);
1559        LOG.debug("Done deleting backup system table snapshot");
1560      } else {
1561        LOG.error("Snapshot " + snapshotName + " does not exists");
1562      }
1563    }
1564  }
1565
1566  private Put createPutForDeleteOperation(String[] backupIdList) {
1567    byte[] value = Bytes.toBytes(StringUtils.join(backupIdList, ","));
1568    Put put = new Put(DELETE_OP_ROW);
1569    put.addColumn(META_FAMILY, FAM_COL, value);
1570    return put;
1571  }
1572
1573  private Delete createDeleteForBackupDeleteOperation() {
1574    Delete delete = new Delete(DELETE_OP_ROW);
1575    delete.addFamily(META_FAMILY);
1576    return delete;
1577  }
1578
1579  private Get createGetForDeleteOperation() {
1580    Get get = new Get(DELETE_OP_ROW);
1581    get.addFamily(META_FAMILY);
1582    return get;
1583  }
1584
1585  public void startDeleteOperation(String[] backupIdList) throws IOException {
1586    if (LOG.isTraceEnabled()) {
1587      LOG.trace("Start delete operation for backups: " + StringUtils.join(backupIdList));
1588    }
1589    Put put = createPutForDeleteOperation(backupIdList);
1590    try (Table table = connection.getTable(tableName)) {
1591      table.put(put);
1592    }
1593  }
1594
1595  public void finishDeleteOperation() throws IOException {
1596    LOG.trace("Finsih delete operation for backup ids");
1597
1598    Delete delete = createDeleteForBackupDeleteOperation();
1599    try (Table table = connection.getTable(tableName)) {
1600      table.delete(delete);
1601    }
1602  }
1603
1604  public String[] getListOfBackupIdsFromDeleteOperation() throws IOException {
1605    LOG.trace("Get delete operation for backup ids");
1606
1607    Get get = createGetForDeleteOperation();
1608    try (Table table = connection.getTable(tableName)) {
1609      Result res = table.get(get);
1610      if (res.isEmpty()) {
1611        return null;
1612      }
1613      Cell cell = res.listCells().get(0);
1614      byte[] val = CellUtil.cloneValue(cell);
1615      if (val.length == 0) {
1616        return null;
1617      }
1618      return Splitter.on(',').splitToStream(new String(val, StandardCharsets.UTF_8))
1619        .toArray(String[]::new);
1620    }
1621  }
1622
1623  private Put createPutForMergeOperation(String[] backupIdList) {
1624    byte[] value = Bytes.toBytes(StringUtils.join(backupIdList, ","));
1625    Put put = new Put(MERGE_OP_ROW);
1626    put.addColumn(META_FAMILY, FAM_COL, value);
1627    return put;
1628  }
1629
1630  public boolean isMergeInProgress() throws IOException {
1631    Get get = new Get(MERGE_OP_ROW);
1632    try (Table table = connection.getTable(tableName)) {
1633      Result res = table.get(get);
1634      return !res.isEmpty();
1635    }
1636  }
1637
1638  private Put createPutForUpdateTablesForMerge(List<TableName> tables) {
1639    byte[] value = Bytes.toBytes(StringUtils.join(tables, ","));
1640    Put put = new Put(MERGE_OP_ROW);
1641    put.addColumn(META_FAMILY, PATH_COL, value);
1642    return put;
1643  }
1644
1645  private Delete createDeleteForBackupMergeOperation() {
1646    Delete delete = new Delete(MERGE_OP_ROW);
1647    delete.addFamily(META_FAMILY);
1648    return delete;
1649  }
1650
1651  private Get createGetForMergeOperation() {
1652    Get get = new Get(MERGE_OP_ROW);
1653    get.addFamily(META_FAMILY);
1654    return get;
1655  }
1656
1657  public void startMergeOperation(String[] backupIdList) throws IOException {
1658    if (LOG.isTraceEnabled()) {
1659      LOG.trace("Start merge operation for backups: " + StringUtils.join(backupIdList));
1660    }
1661    Put put = createPutForMergeOperation(backupIdList);
1662    try (Table table = connection.getTable(tableName)) {
1663      table.put(put);
1664    }
1665  }
1666
1667  public void updateProcessedTablesForMerge(List<TableName> tables) throws IOException {
1668    if (LOG.isTraceEnabled()) {
1669      LOG.trace("Update tables for merge : " + StringUtils.join(tables, ","));
1670    }
1671    Put put = createPutForUpdateTablesForMerge(tables);
1672    try (Table table = connection.getTable(tableName)) {
1673      table.put(put);
1674    }
1675  }
1676
1677  public void finishMergeOperation() throws IOException {
1678    LOG.trace("Finish merge operation for backup ids");
1679
1680    Delete delete = createDeleteForBackupMergeOperation();
1681    try (Table table = connection.getTable(tableName)) {
1682      table.delete(delete);
1683    }
1684  }
1685
1686  public String[] getListOfBackupIdsFromMergeOperation() throws IOException {
1687    LOG.trace("Get backup ids for merge operation");
1688
1689    Get get = createGetForMergeOperation();
1690    try (Table table = connection.getTable(tableName)) {
1691      Result res = table.get(get);
1692      if (res.isEmpty()) {
1693        return null;
1694      }
1695      Cell cell = res.listCells().get(0);
1696      byte[] val = CellUtil.cloneValue(cell);
1697      if (val.length == 0) {
1698        return null;
1699      }
1700      return Splitter.on(',').splitToStream(new String(val, StandardCharsets.UTF_8))
1701        .toArray(String[]::new);
1702    }
1703  }
1704
1705  /**
1706   * Creates a scan to read all registered bulk loads for the given table, or for all tables if
1707   * {@code table} is {@code null}.
1708   */
1709  static Scan createScanForOrigBulkLoadedFiles(@Nullable TableName table) {
1710    Scan scan = new Scan();
1711    byte[] startRow = table == null
1712      ? BULK_LOAD_PREFIX_BYTES
1713      : rowkey(BULK_LOAD_PREFIX, table.toString(), BLK_LD_DELIM);
1714    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1715    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1716    scan.withStartRow(startRow);
1717    scan.withStopRow(stopRow);
1718    scan.addFamily(BackupSystemTable.META_FAMILY);
1719    scan.readVersions(1);
1720    return scan;
1721  }
1722
1723  static String getTableNameFromOrigBulkLoadRow(String rowStr) {
1724    // format is bulk : namespace : table : region : file
1725    return Iterators.get(Splitter.onPattern(BLK_LD_DELIM).split(rowStr).iterator(), 1);
1726  }
1727
1728  static String getRegionNameFromOrigBulkLoadRow(String rowStr) {
1729    // format is bulk : namespace : table : region : file
1730    List<String> parts = Splitter.onPattern(BLK_LD_DELIM).splitToList(rowStr);
1731    Iterator<String> i = parts.iterator();
1732    int idx = 3;
1733    if (parts.size() == 4) {
1734      // the table is in default namespace
1735      idx = 2;
1736    }
1737    String region = Iterators.get(i, idx);
1738    LOG.debug("bulk row string " + rowStr + " region " + region);
1739    return region;
1740  }
1741
1742  /*
1743   * Used to query bulk loaded hfiles which have been copied by incremental backup
1744   * @param backupId the backup Id. It can be null when querying for all tables
1745   * @return the Scan object
1746   * @deprecated This method is broken if a backupId is specified - see HBASE-28715
1747   */
1748  static Scan createScanForBulkLoadedFiles(String backupId) {
1749    Scan scan = new Scan();
1750    byte[] startRow =
1751      backupId == null ? BULK_LOAD_PREFIX_BYTES : rowkey(BULK_LOAD_PREFIX, backupId + BLK_LD_DELIM);
1752    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1753    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1754    scan.withStartRow(startRow);
1755    scan.withStopRow(stopRow);
1756    scan.addFamily(BackupSystemTable.META_FAMILY);
1757    scan.readVersions(1);
1758    return scan;
1759  }
1760
1761  /**
1762   * Creates Scan operation to load backup set list
1763   * @return scan operation
1764   */
1765  private Scan createScanForBackupSetList() {
1766    Scan scan = new Scan();
1767    byte[] startRow = Bytes.toBytes(SET_KEY_PREFIX);
1768    byte[] stopRow = Arrays.copyOf(startRow, startRow.length);
1769    stopRow[stopRow.length - 1] = (byte) (stopRow[stopRow.length - 1] + 1);
1770    scan.withStartRow(startRow);
1771    scan.withStopRow(stopRow);
1772    scan.addFamily(BackupSystemTable.META_FAMILY);
1773    return scan;
1774  }
1775
1776  /**
1777   * Creates Get operation to load backup set content
1778   * @return get operation
1779   */
1780  private Get createGetForBackupSet(String name) {
1781    Get get = new Get(rowkey(SET_KEY_PREFIX, name));
1782    get.addFamily(BackupSystemTable.META_FAMILY);
1783    return get;
1784  }
1785
1786  /**
1787   * Creates Delete operation to delete backup set content
1788   * @param name backup set's name
1789   * @return delete operation
1790   */
1791  private Delete createDeleteForBackupSet(String name) {
1792    Delete del = new Delete(rowkey(SET_KEY_PREFIX, name));
1793    del.addFamily(BackupSystemTable.META_FAMILY);
1794    return del;
1795  }
1796
1797  /**
1798   * Creates Put operation to update backup set content
1799   * @param name   backup set's name
1800   * @param tables list of tables
1801   * @return put operation
1802   */
1803  private Put createPutForBackupSet(String name, String[] tables) {
1804    Put put = new Put(rowkey(SET_KEY_PREFIX, name));
1805    byte[] value = convertToByteArray(tables);
1806    put.addColumn(BackupSystemTable.META_FAMILY, Bytes.toBytes("tables"), value);
1807    return put;
1808  }
1809
1810  private byte[] convertToByteArray(String[] tables) {
1811    return Bytes.toBytes(StringUtils.join(tables, ","));
1812  }
1813
1814  /**
1815   * Converts cell to backup set list.
1816   * @param current current cell
1817   * @return backup set as array of table names
1818   */
1819  private String[] cellValueToBackupSet(Cell current) {
1820    byte[] data = CellUtil.cloneValue(current);
1821    if (!ArrayUtils.isEmpty(data)) {
1822      return Bytes.toString(data).split(",");
1823    }
1824    return new String[0];
1825  }
1826
1827  /**
1828   * Converts cell key to backup set name.
1829   * @param current current cell
1830   * @return backup set name
1831   */
1832  private String cellKeyToBackupSetName(Cell current) {
1833    byte[] data = CellUtil.cloneRow(current);
1834    return Bytes.toString(data).substring(SET_KEY_PREFIX.length());
1835  }
1836
1837  private static byte[] rowkey(String s, String... other) {
1838    StringBuilder sb = new StringBuilder(s);
1839    for (String ss : other) {
1840      sb.append(ss);
1841    }
1842    return Bytes.toBytes(sb.toString());
1843  }
1844
1845  private static void ensureTableEnabled(Admin admin, TableName tableName) throws IOException {
1846    if (!admin.isTableEnabled(tableName)) {
1847      try {
1848        admin.enableTable(tableName);
1849      } catch (TableNotDisabledException ignored) {
1850        LOG.info("Table {} is not disabled, ignoring enable request", tableName);
1851      }
1852    }
1853  }
1854
1855  public enum Order {
1856    /**
1857     * Old backups first, most recents last. I.e. sorted by ascending backupId.
1858     */
1859    OLD_TO_NEW,
1860    /**
1861     * New backups first, oldest last. I.e. sorted by descending backupId.
1862     */
1863    NEW_TO_OLD
1864  }
1865}