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 java.io.Closeable;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.HConstants;
029import org.apache.hadoop.hbase.TableName;
030import org.apache.hadoop.hbase.backup.BackupHFileCleaner;
031import org.apache.hadoop.hbase.backup.BackupInfo;
032import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
033import org.apache.hadoop.hbase.backup.BackupObserver;
034import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
035import org.apache.hadoop.hbase.backup.BackupType;
036import org.apache.hadoop.hbase.backup.master.BackupLogCleaner;
037import org.apache.hadoop.hbase.backup.master.LogRollMasterProcedureManager;
038import org.apache.hadoop.hbase.backup.regionserver.LogRollRegionServerProcedureManager;
039import org.apache.hadoop.hbase.client.Admin;
040import org.apache.hadoop.hbase.client.Connection;
041import org.apache.hadoop.hbase.client.TableDescriptor;
042import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
043import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
044import org.apache.hadoop.hbase.procedure.ProcedureManagerHost;
045import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
046import org.apache.hadoop.hbase.util.Pair;
047import org.apache.yetus.audience.InterfaceAudience;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * Handles backup requests, creates backup info records in backup system table to keep track of
053 * backup sessions, dispatches backup request.
054 */
055@InterfaceAudience.Private
056public class BackupManager implements Closeable {
057  // in seconds
058  public final static String BACKUP_EXCLUSIVE_OPERATION_TIMEOUT_SECONDS_KEY =
059    "hbase.backup.exclusive.op.timeout.seconds";
060  // In seconds
061  private final static int DEFAULT_BACKUP_EXCLUSIVE_OPERATION_TIMEOUT = 3600;
062  private static final Logger LOG = LoggerFactory.getLogger(BackupManager.class);
063
064  protected Configuration conf = null;
065  protected BackupInfo backupInfo = null;
066  protected BackupSystemTable systemTable;
067  protected final Connection conn;
068
069  /**
070   * Backup manager constructor.
071   * @param conn connection
072   * @param conf configuration
073   * @throws IOException exception
074   */
075  public BackupManager(Connection conn, Configuration conf) throws IOException {
076    if (
077      !conf.getBoolean(BackupRestoreConstants.BACKUP_ENABLE_KEY,
078        BackupRestoreConstants.BACKUP_ENABLE_DEFAULT)
079    ) {
080      throw new BackupException("HBase backup is not enabled. Check your "
081        + BackupRestoreConstants.BACKUP_ENABLE_KEY + " setting.");
082    }
083    this.conf = conf;
084    this.conn = conn;
085    this.systemTable = new BackupSystemTable(conn);
086  }
087
088  /**
089   * Returns backup info
090   */
091  protected BackupInfo getBackupInfo() {
092    return backupInfo;
093  }
094
095  /**
096   * This method modifies the master's configuration in order to inject backup-related features
097   * (TESTs only)
098   * @param conf configuration
099   */
100  public static void decorateMasterConfiguration(Configuration conf) {
101    if (!isBackupEnabled(conf)) {
102      return;
103    }
104    // Add WAL archive cleaner plug-in
105    String plugins = conf.get(HConstants.HBASE_MASTER_LOGCLEANER_PLUGINS);
106    String cleanerClass = BackupLogCleaner.class.getCanonicalName();
107    if (!plugins.contains(cleanerClass)) {
108      conf.set(HConstants.HBASE_MASTER_LOGCLEANER_PLUGINS, plugins + "," + cleanerClass);
109    }
110
111    String classes = conf.get(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY);
112    String masterProcedureClass = LogRollMasterProcedureManager.class.getName();
113    if (classes == null) {
114      conf.set(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY, masterProcedureClass);
115    } else if (!classes.contains(masterProcedureClass)) {
116      conf.set(ProcedureManagerHost.MASTER_PROCEDURE_CONF_KEY,
117        classes + "," + masterProcedureClass);
118    }
119
120    plugins = conf.get(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS);
121    conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
122      (plugins == null ? "" : plugins + ",") + BackupHFileCleaner.class.getName());
123    if (LOG.isDebugEnabled()) {
124      LOG.debug(
125        "Added log cleaner: {}. Added master procedure manager: {}."
126          + "Added master procedure manager: {}",
127        cleanerClass, masterProcedureClass, BackupHFileCleaner.class.getName());
128    }
129  }
130
131  /**
132   * This method modifies the Region Server configuration in order to inject backup-related features
133   * TESTs only.
134   * @param conf configuration
135   */
136  public static void decorateRegionServerConfiguration(Configuration conf) {
137    if (!isBackupEnabled(conf)) {
138      return;
139    }
140
141    String classes = conf.get(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY);
142    String regionProcedureClass = LogRollRegionServerProcedureManager.class.getName();
143    if (classes == null) {
144      conf.set(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY, regionProcedureClass);
145    } else if (!classes.contains(regionProcedureClass)) {
146      conf.set(ProcedureManagerHost.REGIONSERVER_PROCEDURE_CONF_KEY,
147        classes + "," + regionProcedureClass);
148    }
149    String coproc = conf.get(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
150    String regionObserverClass = BackupObserver.class.getName();
151    conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
152      (coproc == null ? "" : coproc + ",") + regionObserverClass);
153    if (LOG.isDebugEnabled()) {
154      LOG.debug("Added region procedure manager: {}. Added region observer: {}",
155        regionProcedureClass, regionObserverClass);
156    }
157  }
158
159  public static boolean isBackupEnabled(Configuration conf) {
160    return conf.getBoolean(BackupRestoreConstants.BACKUP_ENABLE_KEY,
161      BackupRestoreConstants.BACKUP_ENABLE_DEFAULT);
162  }
163
164  /**
165   * Get configuration
166   */
167  Configuration getConf() {
168    return conf;
169  }
170
171  /**
172   * Stop all the work of backup.
173   */
174  @Override
175  public void close() {
176    if (systemTable != null) {
177      try {
178        systemTable.close();
179      } catch (Exception e) {
180        LOG.error(e.toString(), e);
181      }
182    }
183  }
184
185  /**
186   * Creates a backup info based on input backup request.
187   * @param backupId      backup id
188   * @param type          type
189   * @param tableList     table list
190   * @param targetRootDir root dir
191   * @param workers       number of parallel workers
192   * @param bandwidth     bandwidth per worker in MB per sec
193   * @throws BackupException exception
194   */
195  public BackupInfo createBackupInfo(String backupId, BackupType type, List<TableName> tableList,
196    String targetRootDir, int workers, long bandwidth, boolean noChecksumVerify)
197    throws BackupException {
198    if (targetRootDir == null) {
199      throw new BackupException("Wrong backup request parameter: target backup root directory");
200    }
201
202    if (type == BackupType.FULL && (tableList == null || tableList.isEmpty())) {
203      // If table list is null for full backup, which means backup all tables. Then fill the table
204      // list with all user tables from meta. It no table available, throw the request exception.
205      List<TableDescriptor> htds = null;
206      try (Admin admin = conn.getAdmin()) {
207        htds = admin.listTableDescriptors();
208      } catch (Exception e) {
209        throw new BackupException(e);
210      }
211
212      if (htds == null) {
213        throw new BackupException("No table exists for full backup of all tables.");
214      } else {
215        tableList = new ArrayList<>();
216        for (TableDescriptor hTableDescriptor : htds) {
217          TableName tn = hTableDescriptor.getTableName();
218          if (tn.equals(BackupSystemTable.getTableName(conf))) {
219            // skip backup system table
220            continue;
221          }
222          tableList.add(hTableDescriptor.getTableName());
223        }
224
225        LOG.info("Full backup all the tables available in the cluster: {}", tableList);
226      }
227    }
228
229    // there are one or more tables in the table list
230    backupInfo = new BackupInfo(backupId, type, tableList.toArray(new TableName[tableList.size()]),
231      targetRootDir);
232    backupInfo.setBandwidth(bandwidth);
233    backupInfo.setWorkers(workers);
234    backupInfo.setNoChecksumVerify(noChecksumVerify);
235    return backupInfo;
236  }
237
238  /**
239   * Check if any ongoing backup. Currently, we only reply on checking status in backup system
240   * table. We need to consider to handle the case of orphan records in the future. Otherwise, all
241   * the coming request will fail.
242   * @return the ongoing backup id if on going backup exists, otherwise null
243   * @throws IOException exception
244   */
245  private String getOngoingBackupId() throws IOException {
246    ArrayList<BackupInfo> sessions = systemTable.getBackupInfos(BackupState.RUNNING);
247    if (sessions.size() == 0) {
248      return null;
249    }
250    return sessions.get(0).getBackupId();
251  }
252
253  /**
254   * Start the backup manager service.
255   * @throws IOException exception
256   */
257  public void initialize() throws IOException {
258    String ongoingBackupId = this.getOngoingBackupId();
259    if (ongoingBackupId != null) {
260      LOG.info("There is a ongoing backup {}"
261        + ". Can not launch new backup until no ongoing backup remains.", ongoingBackupId);
262      throw new BackupException("There is ongoing backup seesion.");
263    }
264  }
265
266  public void setBackupInfo(BackupInfo backupInfo) {
267    this.backupInfo = backupInfo;
268  }
269
270  /*
271   * backup system table operations
272   */
273
274  /**
275   * Updates status (state) of a backup session in a persistent store
276   * @param context context
277   * @throws IOException exception
278   */
279  public void updateBackupInfo(BackupInfo context) throws IOException {
280    systemTable.updateBackupInfo(context);
281  }
282
283  /**
284   * Starts new backup session
285   * @throws IOException if active session already exists
286   */
287  public void startBackupSession() throws IOException {
288    long startTime = EnvironmentEdgeManager.currentTime();
289    long timeout = conf.getInt(BACKUP_EXCLUSIVE_OPERATION_TIMEOUT_SECONDS_KEY,
290      DEFAULT_BACKUP_EXCLUSIVE_OPERATION_TIMEOUT) * 1000L;
291    long lastWarningOutputTime = 0;
292    while (EnvironmentEdgeManager.currentTime() - startTime < timeout) {
293      try {
294        systemTable.startBackupExclusiveOperation();
295        return;
296      } catch (IOException e) {
297        if (e instanceof ExclusiveOperationException) {
298          // sleep, then repeat
299          try {
300            Thread.sleep(1000);
301          } catch (InterruptedException e1) {
302            // Restore the interrupted status
303            Thread.currentThread().interrupt();
304          }
305          if (
306            lastWarningOutputTime == 0
307              || (EnvironmentEdgeManager.currentTime() - lastWarningOutputTime) > 60000
308          ) {
309            lastWarningOutputTime = EnvironmentEdgeManager.currentTime();
310            LOG.warn("Waiting to acquire backup exclusive lock for {}s",
311              +(lastWarningOutputTime - startTime) / 1000);
312          }
313        } else {
314          throw e;
315        }
316      }
317    }
318    throw new IOException(
319      "Failed to acquire backup system table exclusive lock after " + timeout / 1000 + "s");
320  }
321
322  /**
323   * Finishes active backup session
324   * @throws IOException if no active session
325   */
326  public void finishBackupSession() throws IOException {
327    systemTable.finishBackupExclusiveOperation();
328  }
329
330  /**
331   * Read the last backup start code (timestamp) of last successful backup. Will return null if
332   * there is no startcode stored in backup system table or the value is of length 0. These two
333   * cases indicate there is no successful backup completed so far.
334   * @return the timestamp of a last successful backup
335   * @throws IOException exception
336   */
337  public String readBackupStartCode() throws IOException {
338    return systemTable.readBackupStartCode(backupInfo.getBackupRootDir());
339  }
340
341  /**
342   * Write the start code (timestamp) to backup system table. If passed in null, then write 0 byte.
343   * @param startCode start code
344   * @throws IOException exception
345   */
346  public void writeBackupStartCode(Long startCode) throws IOException {
347    systemTable.writeBackupStartCode(startCode, backupInfo.getBackupRootDir());
348  }
349
350  /**
351   * Get the RS log information after the last log roll from backup system table.
352   * @return RS log info
353   * @throws IOException exception
354   */
355  public HashMap<String, Long> readRegionServerLastLogRollResult() throws IOException {
356    return systemTable.readRegionServerLastLogRollResult(backupInfo.getBackupRootDir());
357  }
358
359  public Pair<Map<TableName, Map<String, Map<String, List<Pair<String, Boolean>>>>>, List<byte[]>>
360    readBulkloadRows(List<TableName> tableList) throws IOException {
361    return systemTable.readBulkloadRows(tableList);
362  }
363
364  public void deleteBulkLoadedRows(List<byte[]> rows) throws IOException {
365    systemTable.deleteBulkLoadedRows(rows);
366  }
367
368  /**
369   * Get all completed backup information (in desc order by time)
370   * @return history info of BackupCompleteData
371   * @throws IOException exception
372   */
373  public List<BackupInfo> getBackupHistory() throws IOException {
374    return systemTable.getBackupHistory();
375  }
376
377  public ArrayList<BackupInfo> getBackupHistory(boolean completed) throws IOException {
378    return systemTable.getBackupHistory(completed);
379  }
380
381  /**
382   * Write the current timestamps for each regionserver to backup system table after a successful
383   * full or incremental backup. Each table may have a different set of log timestamps. The saved
384   * timestamp is of the last log file that was backed up already.
385   * @param tables tables
386   * @throws IOException exception
387   */
388  public void writeRegionServerLogTimestamp(Set<TableName> tables, Map<String, Long> newTimestamps)
389    throws IOException {
390    systemTable.writeRegionServerLogTimestamp(tables, newTimestamps, backupInfo.getBackupRootDir());
391  }
392
393  /**
394   * Read the timestamp for each region server log after the last successful backup. Each table has
395   * its own set of the timestamps.
396   * @return the timestamp for each region server. key: tableName value:
397   *         RegionServer,PreviousTimeStamp
398   * @throws IOException exception
399   */
400  public Map<TableName, Map<String, Long>> readLogTimestampMap() throws IOException {
401    return systemTable.readLogTimestampMap(backupInfo.getBackupRootDir());
402  }
403
404  /**
405   * Return the current tables covered by incremental backup.
406   * @return set of tableNames
407   * @throws IOException exception
408   */
409  public Set<TableName> getIncrementalBackupTableSet() throws IOException {
410    return systemTable.getIncrementalBackupTableSet(backupInfo.getBackupRootDir());
411  }
412
413  /**
414   * Adds set of tables to overall incremental backup table set
415   * @param tables tables
416   * @throws IOException exception
417   */
418  public void addIncrementalBackupTableSet(Set<TableName> tables) throws IOException {
419    systemTable.addIncrementalBackupTableSet(tables, backupInfo.getBackupRootDir());
420  }
421
422  public Connection getConnection() {
423    return conn;
424  }
425}