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.snapshot;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033import java.util.TreeMap;
034import java.util.concurrent.ThreadPoolExecutor;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.fs.FileStatus;
037import org.apache.hadoop.fs.FileSystem;
038import org.apache.hadoop.fs.Path;
039import org.apache.hadoop.hbase.MetaTableAccessor;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.backup.HFileArchiver;
042import org.apache.hadoop.hbase.client.Connection;
043import org.apache.hadoop.hbase.client.ConnectionFactory;
044import org.apache.hadoop.hbase.client.RegionInfo;
045import org.apache.hadoop.hbase.client.RegionInfoBuilder;
046import org.apache.hadoop.hbase.client.TableDescriptor;
047import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
048import org.apache.hadoop.hbase.io.HFileLink;
049import org.apache.hadoop.hbase.io.Reference;
050import org.apache.hadoop.hbase.mob.MobUtils;
051import org.apache.hadoop.hbase.monitoring.MonitoredTask;
052import org.apache.hadoop.hbase.monitoring.TaskMonitor;
053import org.apache.hadoop.hbase.regionserver.HRegion;
054import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
055import org.apache.hadoop.hbase.regionserver.StoreContext;
056import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
057import org.apache.hadoop.hbase.regionserver.StoreUtils;
058import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker;
059import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory;
060import org.apache.hadoop.hbase.security.access.AccessControlClient;
061import org.apache.hadoop.hbase.security.access.Permission;
062import org.apache.hadoop.hbase.security.access.ShadedAccessControlUtil;
063import org.apache.hadoop.hbase.security.access.TablePermission;
064import org.apache.hadoop.hbase.util.Bytes;
065import org.apache.hadoop.hbase.util.CommonFSUtils;
066import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
067import org.apache.hadoop.hbase.util.FSUtils;
068import org.apache.hadoop.hbase.util.ModifyRegionUtils;
069import org.apache.hadoop.hbase.util.Pair;
070import org.apache.hadoop.io.IOUtils;
071import org.apache.yetus.audience.InterfaceAudience;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074
075import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap;
076
077import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
078import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
079import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
080
081/**
082 * Helper to Restore/Clone a Snapshot
083 * <p>
084 * The helper assumes that a table is already created, and by calling restore() the content present
085 * in the snapshot will be restored as the new content of the table.
086 * <p>
087 * Clone from Snapshot: If the target table is empty, the restore operation is just a "clone
088 * operation", where the only operations are:
089 * <ul>
090 * <li>for each region in the snapshot create a new region (note that the region will have a
091 * different name, since the encoding contains the table name)
092 * <li>for each file in the region create a new HFileLink to point to the original file.
093 * <li>restore the logs, if any
094 * </ul>
095 * <p>
096 * Restore from Snapshot:
097 * <ul>
098 * <li>for each region in the table verify which are available in the snapshot and which are not
099 * <ul>
100 * <li>if the region is not present in the snapshot, remove it.
101 * <li>if the region is present in the snapshot
102 * <ul>
103 * <li>for each file in the table region verify which are available in the snapshot
104 * <ul>
105 * <li>if the hfile is not present in the snapshot, remove it
106 * <li>if the hfile is present, keep it (nothing to do)
107 * </ul>
108 * <li>for each file in the snapshot region but not in the table
109 * <ul>
110 * <li>create a new HFileLink that point to the original file
111 * </ul>
112 * </ul>
113 * </ul>
114 * <li>for each region in the snapshot not present in the current table state
115 * <ul>
116 * <li>create a new region and for each file in the region create a new HFileLink (This is the same
117 * as the clone operation)
118 * </ul>
119 * <li>restore the logs, if any
120 * </ul>
121 */
122@InterfaceAudience.Private
123public class RestoreSnapshotHelper {
124  private static final Logger LOG = LoggerFactory.getLogger(RestoreSnapshotHelper.class);
125
126  private final Map<byte[], byte[]> regionsMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
127
128  private final Map<String, Pair<String, String>> parentsMap = new HashMap<>();
129
130  private final ForeignExceptionDispatcher monitor;
131  private final MonitoredTask status;
132
133  private final SnapshotManifest snapshotManifest;
134  private final SnapshotDescription snapshotDesc;
135  private final TableName snapshotTable;
136
137  private final TableDescriptor tableDesc;
138  private final Path rootDir;
139  private final Path tableDir;
140
141  private final Configuration conf;
142  private final FileSystem fs;
143  private final boolean createBackRefs;
144
145  public RestoreSnapshotHelper(final Configuration conf, final FileSystem fs,
146    final SnapshotManifest manifest, final TableDescriptor tableDescriptor, final Path rootDir,
147    final ForeignExceptionDispatcher monitor, final MonitoredTask status) {
148    this(conf, fs, manifest, tableDescriptor, rootDir, monitor, status, true);
149  }
150
151  public RestoreSnapshotHelper(final Configuration conf, final FileSystem fs,
152    final SnapshotManifest manifest, final TableDescriptor tableDescriptor, final Path rootDir,
153    final ForeignExceptionDispatcher monitor, final MonitoredTask status,
154    final boolean createBackRefs) {
155    this.fs = fs;
156    this.conf = conf;
157    this.snapshotManifest = manifest;
158    this.snapshotDesc = manifest.getSnapshotDescription();
159    this.snapshotTable = TableName.valueOf(snapshotDesc.getTable());
160    this.tableDesc = tableDescriptor;
161    this.rootDir = rootDir;
162    this.tableDir = CommonFSUtils.getTableDir(rootDir, tableDesc.getTableName());
163    this.monitor = monitor;
164    this.status = status;
165    this.createBackRefs = createBackRefs;
166  }
167
168  /**
169   * Restore the on-disk table to a specified snapshot state.
170   * @return the set of regions touched by the restore operation
171   */
172  public RestoreMetaChanges restoreHdfsRegions() throws IOException {
173    ThreadPoolExecutor exec = SnapshotManifest.createExecutor(conf, "RestoreSnapshot");
174    try {
175      return restoreHdfsRegions(exec);
176    } finally {
177      exec.shutdown();
178    }
179  }
180
181  private RestoreMetaChanges restoreHdfsRegions(final ThreadPoolExecutor exec) throws IOException {
182    LOG.info("starting restore table regions using snapshot=" + snapshotDesc);
183
184    Map<String, SnapshotRegionManifest> regionManifests = snapshotManifest.getRegionManifestsMap();
185    if (regionManifests == null) {
186      LOG.warn("Nothing to restore. Snapshot " + snapshotDesc + " looks empty");
187      return null;
188    }
189
190    RestoreMetaChanges metaChanges = new RestoreMetaChanges(tableDesc, parentsMap);
191
192    // Take a copy of the manifest.keySet() since we are going to modify
193    // this instance, by removing the regions already present in the restore dir.
194    Set<String> regionNames = new HashSet<>(regionManifests.keySet());
195
196    List<RegionInfo> tableRegions = getTableRegions();
197
198    RegionInfo mobRegion =
199      MobUtils.getMobRegionInfo(snapshotManifest.getTableDescriptor().getTableName());
200    if (tableRegions != null) {
201      // restore the mob region in case
202      if (regionNames.contains(mobRegion.getEncodedName())) {
203        monitor.rethrowException();
204        status.setStatus("Restoring mob region...");
205        List<RegionInfo> mobRegions = new ArrayList<>(1);
206        mobRegions.add(mobRegion);
207        restoreHdfsMobRegions(exec, regionManifests, mobRegions);
208        regionNames.remove(mobRegion.getEncodedName());
209        status.setStatus("Finished restoring mob region.");
210      }
211    }
212    if (regionNames.contains(mobRegion.getEncodedName())) {
213      // add the mob region
214      monitor.rethrowException();
215      status.setStatus("Cloning mob region...");
216      cloneHdfsMobRegion(regionManifests, mobRegion);
217      regionNames.remove(mobRegion.getEncodedName());
218      status.setStatus("Finished cloning mob region.");
219    }
220
221    // Identify which region are still available and which not.
222    // NOTE: we rely upon the region name as: "table name, start key, end key"
223    if (tableRegions != null) {
224      monitor.rethrowException();
225      for (RegionInfo regionInfo : tableRegions) {
226        String regionName = regionInfo.getEncodedName();
227        if (regionNames.contains(regionName)) {
228          LOG.info("region to restore: " + regionName);
229          regionNames.remove(regionName);
230          metaChanges.addRegionToRestore(
231            ProtobufUtil.toRegionInfo(regionManifests.get(regionName).getRegionInfo()));
232        } else {
233          LOG.info("region to remove: " + regionName);
234          metaChanges.addRegionToRemove(regionInfo);
235        }
236      }
237    }
238
239    // Regions to Add: present in the snapshot but not in the current table
240    List<RegionInfo> regionsToAdd = new ArrayList<>(regionNames.size());
241    if (regionNames.size() > 0) {
242      monitor.rethrowException();
243      for (String regionName : regionNames) {
244        LOG.info("region to add: " + regionName);
245        regionsToAdd
246          .add(ProtobufUtil.toRegionInfo(regionManifests.get(regionName).getRegionInfo()));
247      }
248    }
249
250    // Create new regions cloning from the snapshot
251    // HBASE-19980: We need to call cloneHdfsRegions() before restoreHdfsRegions() because
252    // regionsMap is constructed in cloneHdfsRegions() and it can be used in restoreHdfsRegions().
253    monitor.rethrowException();
254    status.setStatus("Cloning regions...");
255    RegionInfo[] clonedRegions = cloneHdfsRegions(exec, regionManifests, regionsToAdd);
256    metaChanges.setNewRegions(clonedRegions);
257    status.setStatus("Finished cloning regions.");
258
259    // Restore regions using the snapshot data
260    monitor.rethrowException();
261    status.setStatus("Restoring table regions...");
262    restoreHdfsRegions(exec, regionManifests, metaChanges.getRegionsToRestore());
263    status.setStatus("Finished restoring all table regions.");
264
265    // Remove regions from the current table
266    monitor.rethrowException();
267    status.setStatus("Starting to delete excess regions from table");
268    removeHdfsRegions(exec, metaChanges.getRegionsToRemove());
269    status.setStatus("Finished deleting excess regions from table.");
270
271    LOG.info("finishing restore table regions using snapshot=" + snapshotDesc);
272
273    return metaChanges;
274  }
275
276  /**
277   * Describe the set of operations needed to update hbase:meta after restore.
278   */
279  public static class RestoreMetaChanges {
280    private final Map<String, Pair<String, String>> parentsMap;
281    private final TableDescriptor htd;
282
283    private List<RegionInfo> regionsToRestore = null;
284    private List<RegionInfo> regionsToRemove = null;
285    private List<RegionInfo> regionsToAdd = null;
286
287    public RestoreMetaChanges(TableDescriptor htd, Map<String, Pair<String, String>> parentsMap) {
288      this.parentsMap = parentsMap;
289      this.htd = htd;
290    }
291
292    public TableDescriptor getTableDescriptor() {
293      return htd;
294    }
295
296    /**
297     * Returns the map of parent-children_pair.
298     * @return the map
299     */
300    public Map<String, Pair<String, String>> getParentToChildrenPairMap() {
301      return this.parentsMap;
302    }
303
304    /** Returns true if there're new regions */
305    public boolean hasRegionsToAdd() {
306      return this.regionsToAdd != null && this.regionsToAdd.size() > 0;
307    }
308
309    /**
310     * Returns the list of new regions added during the on-disk restore. The caller is responsible
311     * to add the regions to META. e.g MetaTableAccessor.addRegionsToMeta(...)
312     * @return the list of regions to add to META
313     */
314    public List<RegionInfo> getRegionsToAdd() {
315      return this.regionsToAdd;
316    }
317
318    /** Returns true if there're regions to restore */
319    public boolean hasRegionsToRestore() {
320      return this.regionsToRestore != null && this.regionsToRestore.size() > 0;
321    }
322
323    /**
324     * Returns the list of 'restored regions' during the on-disk restore. The caller is responsible
325     * to add the regions to hbase:meta if not present.
326     * @return the list of regions restored
327     */
328    public List<RegionInfo> getRegionsToRestore() {
329      return this.regionsToRestore;
330    }
331
332    /** Returns true if there're regions to remove */
333    public boolean hasRegionsToRemove() {
334      return this.regionsToRemove != null && this.regionsToRemove.size() > 0;
335    }
336
337    /**
338     * Returns the list of regions removed during the on-disk restore. The caller is responsible to
339     * remove the regions from META. e.g. MetaTableAccessor.deleteRegions(...)
340     * @return the list of regions to remove from META
341     */
342    public List<RegionInfo> getRegionsToRemove() {
343      return this.regionsToRemove;
344    }
345
346    void setNewRegions(final RegionInfo[] hris) {
347      if (hris != null) {
348        regionsToAdd = Arrays.asList(hris);
349      } else {
350        regionsToAdd = null;
351      }
352    }
353
354    void addRegionToRemove(final RegionInfo hri) {
355      if (regionsToRemove == null) {
356        regionsToRemove = new LinkedList<>();
357      }
358      regionsToRemove.add(hri);
359    }
360
361    void addRegionToRestore(final RegionInfo hri) {
362      if (regionsToRestore == null) {
363        regionsToRestore = new LinkedList<>();
364      }
365      regionsToRestore.add(hri);
366    }
367
368    public void updateMetaParentRegions(Connection connection, final List<RegionInfo> regionInfos)
369      throws IOException {
370      if (regionInfos == null || parentsMap.isEmpty()) return;
371
372      // Extract region names and offlined regions
373      Map<String, RegionInfo> regionsByName = new HashMap<>(regionInfos.size());
374      List<RegionInfo> parentRegions = new LinkedList<>();
375      for (RegionInfo regionInfo : regionInfos) {
376        if (regionInfo.isSplitParent()) {
377          parentRegions.add(regionInfo);
378        } else {
379          regionsByName.put(regionInfo.getEncodedName(), regionInfo);
380        }
381      }
382
383      // Update Offline parents
384      for (RegionInfo regionInfo : parentRegions) {
385        Pair<String, String> daughters = parentsMap.get(regionInfo.getEncodedName());
386        if (daughters == null) {
387          // The snapshot contains an unreferenced region.
388          // It will be removed by the CatalogJanitor.
389          LOG.warn("Skip update of unreferenced offline parent: " + regionInfo);
390          continue;
391        }
392
393        // One side of the split is already compacted
394        if (daughters.getSecond() == null) {
395          daughters.setSecond(daughters.getFirst());
396        }
397
398        LOG.debug("Update splits parent " + regionInfo.getEncodedName() + " -> " + daughters);
399        MetaTableAccessor.addSplitsToParent(connection, regionInfo,
400          regionsByName.get(daughters.getFirst()), regionsByName.get(daughters.getSecond()));
401      }
402    }
403  }
404
405  /**
406   * Remove specified regions from the file-system, using the archiver.
407   */
408  private void removeHdfsRegions(final ThreadPoolExecutor exec, final List<RegionInfo> regions)
409    throws IOException {
410    if (regions == null || regions.isEmpty()) return;
411    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
412      @Override
413      public void editRegion(final RegionInfo hri) throws IOException {
414        HFileArchiver.archiveRegion(conf, fs, hri);
415      }
416    });
417  }
418
419  /**
420   * Restore specified regions by restoring content to the snapshot state.
421   */
422  private void restoreHdfsRegions(final ThreadPoolExecutor exec,
423    final Map<String, SnapshotRegionManifest> regionManifests, final List<RegionInfo> regions)
424    throws IOException {
425    if (regions == null || regions.isEmpty()) return;
426    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
427      @Override
428      public void editRegion(final RegionInfo hri) throws IOException {
429        restoreRegion(hri, regionManifests.get(hri.getEncodedName()));
430      }
431    });
432  }
433
434  /**
435   * Restore specified mob regions by restoring content to the snapshot state.
436   */
437  private void restoreHdfsMobRegions(final ThreadPoolExecutor exec,
438    final Map<String, SnapshotRegionManifest> regionManifests, final List<RegionInfo> regions)
439    throws IOException {
440    if (regions == null || regions.isEmpty()) return;
441    ModifyRegionUtils.editRegions(exec, regions, new ModifyRegionUtils.RegionEditTask() {
442      @Override
443      public void editRegion(final RegionInfo hri) throws IOException {
444        restoreMobRegion(hri, regionManifests.get(hri.getEncodedName()));
445      }
446    });
447  }
448
449  private Map<String, List<SnapshotRegionManifest.StoreFile>>
450    getRegionHFileReferences(final SnapshotRegionManifest manifest) {
451    Map<String, List<SnapshotRegionManifest.StoreFile>> familyMap =
452      new HashMap<>(manifest.getFamilyFilesCount());
453    for (SnapshotRegionManifest.FamilyFiles familyFiles : manifest.getFamilyFilesList()) {
454      familyMap.put(familyFiles.getFamilyName().toStringUtf8(),
455        new ArrayList<>(familyFiles.getStoreFilesList()));
456    }
457    return familyMap;
458  }
459
460  /**
461   * Restore region by removing files not in the snapshot and adding the missing ones from the
462   * snapshot.
463   */
464  private void restoreRegion(final RegionInfo regionInfo,
465    final SnapshotRegionManifest regionManifest) throws IOException {
466    restoreRegion(regionInfo, regionManifest, new Path(tableDir, regionInfo.getEncodedName()));
467  }
468
469  /**
470   * Restore mob region by removing files not in the snapshot and adding the missing ones from the
471   * snapshot.
472   */
473  private void restoreMobRegion(final RegionInfo regionInfo,
474    final SnapshotRegionManifest regionManifest) throws IOException {
475    if (regionManifest == null) {
476      return;
477    }
478    restoreRegion(regionInfo, regionManifest,
479      MobUtils.getMobRegionPath(conf, tableDesc.getTableName()));
480  }
481
482  /**
483   * Restore region by removing files not in the snapshot and adding the missing ones from the
484   * snapshot.
485   */
486  private void restoreRegion(final RegionInfo regionInfo,
487    final SnapshotRegionManifest regionManifest, Path regionDir) throws IOException {
488    Map<String, List<SnapshotRegionManifest.StoreFile>> snapshotFiles =
489      getRegionHFileReferences(regionManifest);
490
491    String tableName = tableDesc.getTableName().getNameAsString();
492    final String snapshotName = snapshotDesc.getName();
493
494    Path regionPath = new Path(tableDir, regionInfo.getEncodedName());
495    HRegionFileSystem regionFS = (fs.exists(regionPath))
496      ? HRegionFileSystem.openRegionFromFileSystem(conf, fs, tableDir, regionInfo, false)
497      : HRegionFileSystem.createRegionOnFileSystem(conf, fs, tableDir, regionInfo);
498
499    // Restore families present in the table
500    for (Path familyDir : FSUtils.getFamilyDirs(fs, regionDir)) {
501      byte[] family = Bytes.toBytes(familyDir.getName());
502
503      Set<String> familyFiles = getTableRegionFamilyFiles(familyDir);
504      List<SnapshotRegionManifest.StoreFile> snapshotFamilyFiles =
505        snapshotFiles.remove(familyDir.getName());
506      List<StoreFileInfo> filesToTrack = new ArrayList<>();
507      if (snapshotFamilyFiles != null) {
508        List<SnapshotRegionManifest.StoreFile> hfilesToAdd = new ArrayList<>();
509        for (SnapshotRegionManifest.StoreFile storeFile : snapshotFamilyFiles) {
510          if (familyFiles.contains(storeFile.getName())) {
511            // HFile already present
512            familyFiles.remove(storeFile.getName());
513            // no need to restore already present files, but we need to add those to tracker
514            filesToTrack
515              .add(new StoreFileInfo(conf, fs, new Path(familyDir, storeFile.getName()), true));
516          } else {
517            // HFile missing
518            hfilesToAdd.add(storeFile);
519          }
520        }
521
522        // Remove hfiles not present in the snapshot
523        for (String hfileName : familyFiles) {
524          Path hfile = new Path(familyDir, hfileName);
525          if (!fs.getFileStatus(hfile).isDirectory()) {
526            LOG.trace("Removing HFile=" + hfileName + " not present in snapshot=" + snapshotName
527              + " from region=" + regionInfo.getEncodedName() + " table=" + tableName);
528            HFileArchiver.archiveStoreFile(conf, fs, regionInfo, tableDir, family, hfile);
529          }
530        }
531
532        // Restore Missing files
533        for (SnapshotRegionManifest.StoreFile storeFile : hfilesToAdd) {
534          LOG.debug("Restoring missing HFileLink " + storeFile.getName() + " of snapshot="
535            + snapshotName + " to region=" + regionInfo.getEncodedName() + " table=" + tableName);
536          String fileName = restoreStoreFile(familyDir, regionInfo, storeFile, createBackRefs);
537          // mark the reference file to be added to tracker
538          filesToTrack.add(new StoreFileInfo(conf, fs, new Path(familyDir, fileName), true));
539        }
540      } else {
541        // Family doesn't exists in the snapshot
542        LOG.trace("Removing family=" + Bytes.toString(family) + " in snapshot=" + snapshotName
543          + " from region=" + regionInfo.getEncodedName() + " table=" + tableName);
544        HFileArchiver.archiveFamilyByFamilyDir(fs, conf, regionInfo, familyDir, family);
545        fs.delete(familyDir, true);
546      }
547
548      StoreFileTracker tracker =
549        StoreFileTrackerFactory.create(conf, true, StoreContext.getBuilder()
550          .withFamilyStoreDirectoryPath(familyDir).withRegionFileSystem(regionFS).build());
551
552      // simply reset list of tracked files with the matching files
553      // and the extra one present in the snapshot
554      tracker.set(filesToTrack);
555    }
556
557    // Add families not present in the table
558    for (Map.Entry<String, List<SnapshotRegionManifest.StoreFile>> familyEntry : snapshotFiles
559      .entrySet()) {
560      Path familyDir = new Path(regionDir, familyEntry.getKey());
561      StoreFileTracker tracker =
562        StoreFileTrackerFactory.create(conf, true, StoreContext.getBuilder()
563          .withFamilyStoreDirectoryPath(familyDir).withRegionFileSystem(regionFS).build());
564      List<StoreFileInfo> files = new ArrayList<>();
565      if (!fs.mkdirs(familyDir)) {
566        throw new IOException("Unable to create familyDir=" + familyDir);
567      }
568
569      for (SnapshotRegionManifest.StoreFile storeFile : familyEntry.getValue()) {
570        LOG.trace("Adding HFileLink (Not present in the table) " + storeFile.getName()
571          + " of snapshot " + snapshotName + " to table=" + tableName);
572        String fileName = restoreStoreFile(familyDir, regionInfo, storeFile, createBackRefs);
573        files.add(new StoreFileInfo(conf, fs, new Path(familyDir, fileName), true));
574      }
575      tracker.set(files);
576    }
577  }
578
579  /** Returns The set of files in the specified family directory. */
580  private Set<String> getTableRegionFamilyFiles(final Path familyDir) throws IOException {
581    FileStatus[] hfiles = CommonFSUtils.listStatus(fs, familyDir);
582    if (hfiles == null) {
583      return Collections.emptySet();
584    }
585
586    Set<String> familyFiles = new HashSet<>(hfiles.length);
587    for (int i = 0; i < hfiles.length; ++i) {
588      String hfileName = hfiles[i].getPath().getName();
589      familyFiles.add(hfileName);
590    }
591
592    return familyFiles;
593  }
594
595  /**
596   * Clone specified regions. For each region create a new region and create a HFileLink for each
597   * hfile.
598   */
599  private RegionInfo[] cloneHdfsRegions(final ThreadPoolExecutor exec,
600    final Map<String, SnapshotRegionManifest> regionManifests, final List<RegionInfo> regions)
601    throws IOException {
602    if (regions == null || regions.isEmpty()) return null;
603
604    final Map<String, RegionInfo> snapshotRegions = new HashMap<>(regions.size());
605    final String snapshotName = snapshotDesc.getName();
606
607    // clone region info (change embedded tableName with the new one)
608    RegionInfo[] clonedRegionsInfo = new RegionInfo[regions.size()];
609    for (int i = 0; i < clonedRegionsInfo.length; ++i) {
610      // clone the region info from the snapshot region info
611      RegionInfo snapshotRegionInfo = regions.get(i);
612      clonedRegionsInfo[i] = cloneRegionInfo(snapshotRegionInfo);
613
614      // add the region name mapping between snapshot and cloned
615      String snapshotRegionName = snapshotRegionInfo.getEncodedName();
616      String clonedRegionName = clonedRegionsInfo[i].getEncodedName();
617      regionsMap.put(Bytes.toBytes(snapshotRegionName), Bytes.toBytes(clonedRegionName));
618      LOG.info("clone region=" + snapshotRegionName + " as " + clonedRegionName + " in snapshot "
619        + snapshotName);
620
621      // Add mapping between cloned region name and snapshot region info
622      snapshotRegions.put(clonedRegionName, snapshotRegionInfo);
623    }
624
625    // create the regions on disk
626    ModifyRegionUtils.createRegions(exec, conf, rootDir, tableDesc, clonedRegionsInfo,
627      new ModifyRegionUtils.RegionFillTask() {
628        @Override
629        public void fillRegion(final HRegion region) throws IOException {
630          RegionInfo snapshotHri = snapshotRegions.get(region.getRegionInfo().getEncodedName());
631          cloneRegion(region, snapshotHri, regionManifests.get(snapshotHri.getEncodedName()));
632        }
633      });
634
635    return clonedRegionsInfo;
636  }
637
638  /**
639   * Clone the mob region. For the region create a new region and create a HFileLink for each hfile.
640   */
641  private void cloneHdfsMobRegion(final Map<String, SnapshotRegionManifest> regionManifests,
642    final RegionInfo region) throws IOException {
643    // clone region info (change embedded tableName with the new one)
644    Path clonedRegionPath = MobUtils.getMobRegionPath(rootDir, tableDesc.getTableName());
645    cloneRegion(MobUtils.getMobRegionInfo(tableDesc.getTableName()), clonedRegionPath, region,
646      regionManifests.get(region.getEncodedName()));
647  }
648
649  /**
650   * Clone region directory content from the snapshot info. Each region is encoded with the table
651   * name, so the cloned region will have a different region name. Instead of copying the hfiles a
652   * HFileLink is created.
653   * @param regionDir {@link Path} cloned dir
654   */
655  private void cloneRegion(final RegionInfo newRegionInfo, final Path regionDir,
656    final RegionInfo snapshotRegionInfo, final SnapshotRegionManifest manifest) throws IOException {
657    final String tableName = tableDesc.getTableName().getNameAsString();
658    final String snapshotName = snapshotDesc.getName();
659    for (SnapshotRegionManifest.FamilyFiles familyFiles : manifest.getFamilyFilesList()) {
660      Path familyDir = new Path(regionDir, familyFiles.getFamilyName().toStringUtf8());
661      List<StoreFileInfo> clonedFiles = new ArrayList<>();
662      for (SnapshotRegionManifest.StoreFile storeFile : familyFiles.getStoreFilesList()) {
663        LOG.info("Adding HFileLink " + storeFile.getName() + " from cloned region " + "in snapshot "
664          + snapshotName + " to table=" + tableName);
665        if (MobUtils.isMobRegionInfo(newRegionInfo)) {
666          String mobFileName =
667            HFileLink.createHFileLinkName(snapshotRegionInfo, storeFile.getName());
668          Path mobPath = new Path(familyDir, mobFileName);
669          if (fs.exists(mobPath)) {
670            fs.delete(mobPath, true);
671          }
672          restoreStoreFile(familyDir, snapshotRegionInfo, storeFile, createBackRefs);
673        } else {
674          String file = restoreStoreFile(familyDir, snapshotRegionInfo, storeFile, createBackRefs);
675          clonedFiles.add(new StoreFileInfo(conf, fs, new Path(familyDir, file), true));
676        }
677      }
678      // we don't need to track files under mobdir
679      if (!MobUtils.isMobRegionInfo(newRegionInfo)) {
680        Path regionPath = new Path(tableDir, newRegionInfo.getEncodedName());
681        HRegionFileSystem regionFS = (fs.exists(regionPath))
682          ? HRegionFileSystem.openRegionFromFileSystem(conf, fs, tableDir, newRegionInfo, false)
683          : HRegionFileSystem.createRegionOnFileSystem(conf, fs, tableDir, newRegionInfo);
684
685        Configuration sftConf = StoreUtils.createStoreConfiguration(conf, tableDesc,
686          tableDesc.getColumnFamily(familyFiles.getFamilyName().toByteArray()));
687        StoreFileTracker tracker =
688          StoreFileTrackerFactory.create(sftConf, true, StoreContext.getBuilder()
689            .withFamilyStoreDirectoryPath(familyDir).withRegionFileSystem(regionFS).build());
690        tracker.set(clonedFiles);
691      }
692    }
693
694  }
695
696  /**
697   * Clone region directory content from the snapshot info. Each region is encoded with the table
698   * name, so the cloned region will have a different region name. Instead of copying the hfiles a
699   * HFileLink is created.
700   * @param region {@link HRegion} cloned
701   */
702  private void cloneRegion(final HRegion region, final RegionInfo snapshotRegionInfo,
703    final SnapshotRegionManifest manifest) throws IOException {
704    cloneRegion(region.getRegionInfo(), new Path(tableDir, region.getRegionInfo().getEncodedName()),
705      snapshotRegionInfo, manifest);
706  }
707
708  /**
709   * Create a new {@link HFileLink} to reference the store file.
710   * <p>
711   * The store file in the snapshot can be a simple hfile, an HFileLink or a reference.
712   * <ul>
713   * <li>hfile: abc -> table=region-abc
714   * <li>reference: abc.1234 -> table=region-abc.1234
715   * <li>hfilelink: table=region-hfile -> table=region-hfile
716   * </ul>
717   * @param familyDir     destination directory for the store file
718   * @param regionInfo    destination region info for the table
719   * @param createBackRef - Whether back reference should be created. Defaults to true.
720   * @param storeFile     store file name (can be a Reference, HFileLink or simple HFile)
721   */
722  private String restoreStoreFile(final Path familyDir, final RegionInfo regionInfo,
723    final SnapshotRegionManifest.StoreFile storeFile, final boolean createBackRef)
724    throws IOException {
725    String hfileName = storeFile.getName();
726    if (HFileLink.isHFileLink(hfileName)) {
727      return HFileLink.createFromHFileLink(conf, fs, familyDir, hfileName, createBackRef);
728    } else if (StoreFileInfo.isReference(hfileName)) {
729      return restoreReferenceFile(familyDir, regionInfo, storeFile);
730    } else {
731      return HFileLink.create(conf, fs, familyDir, regionInfo, hfileName, createBackRef);
732    }
733  }
734
735  /**
736   * Create a new {@link Reference} as copy of the source one.
737   * <p>
738   * <blockquote>
739   *
740   * <pre>
741   * The source table looks like:
742   *    1234/abc      (original file)
743   *    5678/abc.1234 (reference file)
744   *
745   * After the clone operation looks like:
746   *   wxyz/table=1234-abc
747   *   stuv/table=1234-abc.wxyz
748   *
749   * NOTE that the region name in the clone changes (md5 of regioninfo)
750   * and the reference should reflect that change.
751   * </pre>
752   *
753   * </blockquote>
754   * @param familyDir  destination directory for the store file
755   * @param regionInfo destination region info for the table
756   * @param storeFile  reference file name
757   */
758  private String restoreReferenceFile(final Path familyDir, final RegionInfo regionInfo,
759    final SnapshotRegionManifest.StoreFile storeFile) throws IOException {
760    String hfileName = storeFile.getName();
761
762    // Extract the referred information (hfile name and parent region)
763    Path refPath =
764      StoreFileInfo
765        .getReferredToFile(
766          new Path(
767            new Path(
768              new Path(new Path(snapshotTable.getNamespaceAsString(),
769                snapshotTable.getQualifierAsString()), regionInfo.getEncodedName()),
770              familyDir.getName()),
771            hfileName));
772    String snapshotRegionName = refPath.getParent().getParent().getName();
773    String fileName = refPath.getName();
774
775    // The new reference should have the cloned region name as parent, if it is a clone.
776    String clonedRegionName = Bytes.toString(regionsMap.get(Bytes.toBytes(snapshotRegionName)));
777    if (clonedRegionName == null) clonedRegionName = snapshotRegionName;
778
779    // The output file should be a reference link table=snapshotRegion-fileName.clonedRegionName
780    Path linkPath = null;
781    String refLink = fileName;
782    if (!HFileLink.isHFileLink(fileName)) {
783      refLink = HFileLink.createHFileLinkName(snapshotTable, snapshotRegionName, fileName);
784      linkPath = new Path(familyDir,
785        HFileLink.createHFileLinkName(snapshotTable, regionInfo.getEncodedName(), hfileName));
786    }
787
788    Path outPath = new Path(familyDir, refLink + '.' + clonedRegionName);
789
790    // Create the new reference
791    if (storeFile.hasReference()) {
792      Reference reference = Reference.convert(storeFile.getReference());
793      reference.write(fs, outPath);
794    } else {
795      InputStream in;
796      if (linkPath != null) {
797        in = HFileLink.buildFromHFileLinkPattern(conf, linkPath).open(fs);
798      } else {
799        linkPath = new Path(new Path(
800          HRegion.getRegionDir(snapshotManifest.getSnapshotDir(), regionInfo.getEncodedName()),
801          familyDir.getName()), hfileName);
802        in = fs.open(linkPath);
803      }
804      OutputStream out = fs.create(outPath);
805      IOUtils.copyBytes(in, out, conf);
806    }
807
808    // Add the daughter region to the map
809    String regionName = Bytes.toString(regionsMap.get(regionInfo.getEncodedNameAsBytes()));
810    if (regionName == null) {
811      regionName = regionInfo.getEncodedName();
812    }
813    LOG.debug("Restore reference " + regionName + " to " + clonedRegionName);
814    synchronized (parentsMap) {
815      Pair<String, String> daughters = parentsMap.get(clonedRegionName);
816      if (daughters == null) {
817        // In case one side of the split is already compacted, regionName is put as both first and
818        // second of Pair
819        daughters = new Pair<>(regionName, regionName);
820        parentsMap.put(clonedRegionName, daughters);
821      } else if (!regionName.equals(daughters.getFirst())) {
822        daughters.setSecond(regionName);
823      }
824    }
825    return outPath.getName();
826  }
827
828  /**
829   * Create a new {@link RegionInfo} from the snapshot region info. Keep the same startKey, endKey,
830   * regionId and split information but change the table name.
831   * @param snapshotRegionInfo Info for region to clone.
832   * @return the new HRegion instance
833   */
834  public RegionInfo cloneRegionInfo(final RegionInfo snapshotRegionInfo) {
835    return cloneRegionInfo(tableDesc.getTableName(), snapshotRegionInfo);
836  }
837
838  public static RegionInfo cloneRegionInfo(TableName tableName, RegionInfo snapshotRegionInfo) {
839    return RegionInfoBuilder.newBuilder(tableName).setStartKey(snapshotRegionInfo.getStartKey())
840      .setEndKey(snapshotRegionInfo.getEndKey()).setSplit(snapshotRegionInfo.isSplit())
841      .setRegionId(snapshotRegionInfo.getRegionId()).setOffline(snapshotRegionInfo.isOffline())
842      .build();
843  }
844
845  /** Returns the set of the regions contained in the table */
846  private List<RegionInfo> getTableRegions() throws IOException {
847    LOG.debug("get table regions: " + tableDir);
848    FileStatus[] regionDirs =
849      CommonFSUtils.listStatus(fs, tableDir, new FSUtils.RegionDirFilter(fs));
850    if (regionDirs == null) {
851      return null;
852    }
853
854    List<RegionInfo> regions = new ArrayList<>(regionDirs.length);
855    for (int i = 0; i < regionDirs.length; ++i) {
856      RegionInfo hri = HRegionFileSystem.loadRegionInfoFileContent(fs, regionDirs[i].getPath());
857      regions.add(hri);
858    }
859    LOG.debug("found " + regions.size() + " regions for table="
860      + tableDesc.getTableName().getNameAsString());
861    return regions;
862  }
863
864  /**
865   * Copy the snapshot files for a snapshot scanner, discards meta changes.
866   */
867  public static RestoreMetaChanges copySnapshotForScanner(Configuration conf, FileSystem fs,
868    Path rootDir, Path restoreDir, String snapshotName) throws IOException {
869    // ensure that restore dir is not under root dir
870    if (!restoreDir.getFileSystem(conf).getUri().equals(rootDir.getFileSystem(conf).getUri())) {
871      throw new IllegalArgumentException(
872        "Filesystems for restore directory and HBase root " + "directory should be the same");
873    }
874    if (restoreDir.toUri().getPath().startsWith(rootDir.toUri().getPath() + "/")) {
875      throw new IllegalArgumentException("Restore directory cannot be a sub directory of HBase "
876        + "root directory. RootDir: " + rootDir + ", restoreDir: " + restoreDir);
877    }
878
879    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, rootDir);
880    SnapshotDescription snapshotDesc = SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
881    // check if the snapshot is expired.
882    boolean isExpired = SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDesc.getTtl(),
883      snapshotDesc.getCreationTime(), EnvironmentEdgeManager.currentTime());
884    if (isExpired) {
885      throw new SnapshotTTLExpiredException(ProtobufUtil.createSnapshotDesc(snapshotDesc));
886    }
887    SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, snapshotDesc);
888
889    MonitoredTask status = TaskMonitor.get()
890      .createStatus("Restoring  snapshot '" + snapshotName + "' to directory " + restoreDir);
891    ForeignExceptionDispatcher monitor = new ForeignExceptionDispatcher();
892
893    // we send createBackRefs=false so that restored hfiles do not create back reference links
894    // in the base hbase root dir.
895    RestoreSnapshotHelper helper = new RestoreSnapshotHelper(conf, fs, manifest,
896      manifest.getTableDescriptor(), restoreDir, monitor, status, false);
897    RestoreMetaChanges metaChanges = helper.restoreHdfsRegions(); // TODO: parallelize.
898
899    if (LOG.isDebugEnabled()) {
900      LOG.debug("Restored table dir:" + restoreDir);
901      CommonFSUtils.logFileSystemState(fs, restoreDir, LOG);
902    }
903    return metaChanges;
904  }
905
906  public static void restoreSnapshotAcl(SnapshotDescription snapshot, TableName newTableName,
907    Configuration conf) throws IOException {
908    if (snapshot.hasUsersAndPermissions() && snapshot.getUsersAndPermissions() != null) {
909      LOG.info("Restore snapshot acl to table. snapshot: " + snapshot + ", table: " + newTableName);
910      ListMultimap<String, Permission> perms =
911        ShadedAccessControlUtil.toUserTablePermissions(snapshot.getUsersAndPermissions());
912      try (Connection conn = ConnectionFactory.createConnection(conf)) {
913        for (Entry<String, Permission> e : perms.entries()) {
914          String user = e.getKey();
915          TablePermission tablePerm = (TablePermission) e.getValue();
916          AccessControlClient.grant(conn, newTableName, user, tablePerm.getFamily(),
917            tablePerm.getQualifier(), tablePerm.getActions());
918        }
919      } catch (Throwable e) {
920        throw new IOException("Grant acl into newly creatd table failed. snapshot: " + snapshot
921          + ", table: " + newTableName, e);
922      }
923    }
924  }
925}