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.client;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.UUID;
024import org.apache.hadoop.conf.Configuration;
025import org.apache.hadoop.fs.FileSystem;
026import org.apache.hadoop.fs.Path;
027import org.apache.hadoop.hbase.PrivateCellUtil;
028import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
029import org.apache.hadoop.hbase.mob.MobUtils;
030import org.apache.hadoop.hbase.regionserver.MemStoreLAB;
031import org.apache.hadoop.hbase.snapshot.RestoreSnapshotHelper;
032import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
033import org.apache.hadoop.hbase.snapshot.SnapshotManifest;
034import org.apache.hadoop.hbase.util.CommonFSUtils;
035import org.apache.yetus.audience.InterfaceAudience;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
040import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;
041import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotRegionManifest;
042
043/**
044 * A Scanner which performs a scan over snapshot files. Using this class requires copying the
045 * snapshot to a temporary empty directory, which will copy the snapshot reference files into that
046 * directory. Actual data files are not copied.
047 * <p>
048 * This also allows one to run the scan from an online or offline hbase cluster. The snapshot files
049 * can be exported by using the org.apache.hadoop.hbase.snapshot.ExportSnapshot tool, to a pure-hdfs
050 * cluster, and this scanner can be used to run the scan directly over the snapshot files. The
051 * snapshot should not be deleted while there are open scanners reading from snapshot files.
052 * <p>
053 * An internal RegionScanner is used to execute the {@link Scan} obtained from the user for each
054 * region in the snapshot.
055 * <p>
056 * HBase owns all the data and snapshot files on the filesystem. Only the HBase user can read from
057 * snapshot files and data files. HBase also enforces security because all the requests are handled
058 * by the server layer, and the user cannot read from the data files directly. To read from snapshot
059 * files directly from the file system, the user who is running the MR job must have sufficient
060 * permissions to access snapshot and reference files. This means that to run mapreduce over
061 * snapshot files, the job has to be run as the HBase user or the user must have group or other
062 * priviledges in the filesystem (See HBASE-8369). Note that, given other users access to read from
063 * snapshot/data files will completely circumvent the access control enforced by HBase. See
064 * org.apache.hadoop.hbase.mapreduce.TableSnapshotInputFormat.
065 */
066@InterfaceAudience.Private
067public class TableSnapshotScanner extends AbstractClientScanner {
068
069  private static final Logger LOG = LoggerFactory.getLogger(TableSnapshotScanner.class);
070
071  private Configuration conf;
072  private String snapshotName;
073  private FileSystem fs;
074  private Path rootDir;
075  private Path restoreDir;
076  private Scan scan;
077  private ArrayList<RegionInfo> regions;
078  private TableDescriptor htd;
079  private final boolean snapshotAlreadyRestored;
080
081  private ClientSideRegionScanner currentRegionScanner = null;
082  private int currentRegion = -1;
083
084  private int numOfCompleteRows = 0;
085
086  /**
087   * Creates a TableSnapshotScanner.
088   * @param conf         the configuration
089   * @param restoreDir   a temporary directory to copy the snapshot files into. Current user should
090   *                     have write permissions to this directory, and this should not be a
091   *                     subdirectory of rootDir. The scanner deletes the contents of the directory
092   *                     once the scanner is closed.
093   * @param snapshotName the name of the snapshot to read from
094   * @param scan         a Scan representing scan parameters
095   * @throws IOException in case of error
096   */
097  public TableSnapshotScanner(Configuration conf, Path restoreDir, String snapshotName, Scan scan)
098    throws IOException {
099    this(conf, CommonFSUtils.getRootDir(conf), restoreDir, snapshotName, scan);
100  }
101
102  public TableSnapshotScanner(Configuration conf, Path rootDir, Path restoreDir,
103    String snapshotName, Scan scan) throws IOException {
104    this(conf, rootDir, restoreDir, snapshotName, scan, false);
105  }
106
107  /**
108   * Creates a TableSnapshotScanner.
109   * @param conf                    the configuration
110   * @param rootDir                 root directory for HBase.
111   * @param restoreDir              a temporary directory to copy the snapshot files into. Current
112   *                                user should have write permissions to this directory, and this
113   *                                should not be a subdirectory of rootdir. The scanner deletes the
114   *                                contents of the directory once the scanner is closed.
115   * @param snapshotName            the name of the snapshot to read from
116   * @param scan                    a Scan representing scan parameters
117   * @param snapshotAlreadyRestored true to indicate that snapshot has been restored.
118   * @throws IOException in case of error
119   */
120  public TableSnapshotScanner(Configuration conf, Path rootDir, Path restoreDir,
121    String snapshotName, Scan scan, boolean snapshotAlreadyRestored) throws IOException {
122    this.conf = conf;
123    this.snapshotName = snapshotName;
124    this.rootDir = rootDir;
125    this.scan = scan;
126    this.snapshotAlreadyRestored = snapshotAlreadyRestored;
127    this.fs = rootDir.getFileSystem(conf);
128    conf.setBoolean(MemStoreLAB.USEMSLAB_KEY, false);
129
130    if (snapshotAlreadyRestored) {
131      this.restoreDir = restoreDir;
132      openWithoutRestoringSnapshot();
133    } else {
134      // restoreDir will be deleted in close(), use a unique sub directory
135      this.restoreDir = new Path(restoreDir, UUID.randomUUID().toString());
136      openWithRestoringSnapshot();
137    }
138
139    initScanMetrics(scan);
140  }
141
142  private void openWithoutRestoringSnapshot() throws IOException {
143    Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshotName, rootDir);
144    SnapshotProtos.SnapshotDescription snapshotDesc =
145      SnapshotDescriptionUtils.readSnapshotInfo(fs, snapshotDir);
146
147    SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, snapshotDesc);
148    List<SnapshotRegionManifest> regionManifests = manifest.getRegionManifests();
149    if (regionManifests == null) {
150      throw new IllegalArgumentException("Snapshot seems empty, snapshotName: " + snapshotName);
151    }
152
153    regions = new ArrayList<>(regionManifests.size());
154    regionManifests.stream().map(r -> ProtobufUtil.toRegionInfo(r.getRegionInfo()))
155      .filter(this::isValidRegion).sorted().forEach(r -> regions.add(r));
156    htd = manifest.getTableDescriptor();
157  }
158
159  private boolean isValidRegion(RegionInfo hri) {
160    // An offline split parent region should be excluded.
161    if (hri.isOffline() && (hri.isSplit() || hri.isSplitParent())) {
162      return false;
163    }
164    // The mob region is a dummy region used only to organise mob files under mobdir. It has no
165    // region directory under the table dir to open. See HBASE-30365 and HBASE-30368.
166    if (MobUtils.isMobRegionInfo(hri)) {
167      return false;
168    }
169    return PrivateCellUtil.overlappingKeys(scan.getStartRow(), scan.getStopRow(), hri.getStartKey(),
170      hri.getEndKey());
171  }
172
173  private void openWithRestoringSnapshot() throws IOException {
174    final RestoreSnapshotHelper.RestoreMetaChanges meta =
175      RestoreSnapshotHelper.copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotName);
176    final List<RegionInfo> restoredRegions = meta.getRegionsToAdd();
177
178    htd = meta.getTableDescriptor();
179    regions = new ArrayList<>(restoredRegions.size());
180    restoredRegions.stream().filter(this::isValidRegion).sorted().forEach(r -> regions.add(r));
181  }
182
183  @Override
184  public Result next() throws IOException {
185    Result result = null;
186    while (true) {
187      if (currentRegionScanner == null) {
188        currentRegion++;
189        if (currentRegion >= regions.size()) {
190          return null;
191        }
192
193        RegionInfo hri = regions.get(currentRegion);
194        currentRegionScanner =
195          new ClientSideRegionScanner(conf, fs, restoreDir, htd, hri, scan, scanMetrics);
196        if (this.scanMetrics != null) {
197          this.scanMetrics.addToCounter(ScanMetrics.REGIONS_SCANNED_METRIC_NAME, 1);
198        }
199      }
200
201      try {
202        result = currentRegionScanner.next();
203        if (result != null) {
204          if (scan.getLimit() > 0 && ++this.numOfCompleteRows > scan.getLimit()) {
205            result = null;
206          }
207          return result;
208        }
209      } finally {
210        if (result == null) {
211          currentRegionScanner.close();
212          currentRegionScanner = null;
213        }
214      }
215    }
216  }
217
218  private void cleanup() {
219    try {
220      if (fs.exists(this.restoreDir)) {
221        if (!fs.delete(this.restoreDir, true)) {
222          LOG.warn(
223            "Delete restore directory for the snapshot failed. restoreDir: " + this.restoreDir);
224        }
225      }
226    } catch (IOException ex) {
227      LOG.warn(
228        "Could not delete restore directory for the snapshot. restoreDir: " + this.restoreDir, ex);
229    }
230  }
231
232  @Override
233  public void close() {
234    if (currentRegionScanner != null) {
235      currentRegionScanner.close();
236    }
237    // if snapshotAlreadyRestored is true, then we should invoke cleanup() method by hand.
238    if (!this.snapshotAlreadyRestored) {
239      cleanup();
240    }
241  }
242
243  @Override
244  public boolean renewLease() {
245    throw new UnsupportedOperationException();
246  }
247
248}