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