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.io;
019
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.util.Optional;
023import java.util.concurrent.atomic.AtomicBoolean;
024import java.util.function.IntConsumer;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.ExtendedCell;
027import org.apache.hadoop.hbase.HConstants;
028import org.apache.hadoop.hbase.KeyValue;
029import org.apache.hadoop.hbase.PrivateCellUtil;
030import org.apache.hadoop.hbase.client.Scan;
031import org.apache.hadoop.hbase.io.hfile.CacheConfig;
032import org.apache.hadoop.hbase.io.hfile.HFileInfo;
033import org.apache.hadoop.hbase.io.hfile.HFileReaderImpl;
034import org.apache.hadoop.hbase.io.hfile.HFileScanner;
035import org.apache.hadoop.hbase.io.hfile.ReaderContext;
036import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
037import org.apache.hadoop.hbase.regionserver.StoreFileReader;
038import org.apache.hadoop.hbase.util.Bytes;
039import org.apache.yetus.audience.InterfaceAudience;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * A facade for a {@link org.apache.hadoop.hbase.io.hfile.HFile.Reader} that serves up either the
045 * top or bottom half of a HFile where 'bottom' is the first half of the file containing the keys
046 * that sort lowest and 'top' is the second half of the file with keys that sort greater than those
047 * of the bottom half. The top includes the split files midkey, of the key that follows if it does
048 * not exist in the file.
049 * <p>
050 * This type works in tandem with the {@link Reference} type. This class is used reading while
051 * Reference is used writing.
052 * <p>
053 * This file is not splitable. Calls to {@link #midKey()} return null.
054 */
055@InterfaceAudience.Private
056public class HalfStoreFileReader extends StoreFileReader {
057  private static final Logger LOG = LoggerFactory.getLogger(HalfStoreFileReader.class);
058  final boolean top;
059  // This is the key we split around. Its the first possible entry on a row:
060  // i.e. empty column and a timestamp of LATEST_TIMESTAMP.
061  protected final byte[] splitkey;
062
063  private final ExtendedCell splitCell;
064
065  private Optional<ExtendedCell> firstKey = Optional.empty();
066
067  private boolean firstKeySeeked = false;
068
069  private AtomicBoolean closed = new AtomicBoolean(false);
070
071  /**
072   * Creates a half file reader for a hfile referred to by an hfilelink.
073   * @param context   Reader context info
074   * @param fileInfo  HFile info
075   * @param cacheConf CacheConfig
076   * @param r         original reference file (contains top or bottom)
077   * @param conf      Configuration
078   */
079  public HalfStoreFileReader(final ReaderContext context, final HFileInfo fileInfo,
080    final CacheConfig cacheConf, final Reference r, StoreFileInfo storeFileInfo,
081    final Configuration conf) throws IOException {
082    super(context, fileInfo, cacheConf, storeFileInfo, conf);
083    // This is not actual midkey for this half-file; its just border
084    // around which we split top and bottom. Have to look in files to find
085    // actual last and first keys for bottom and top halves. Half-files don't
086    // have an actual midkey themselves. No midkey is how we indicate file is
087    // not splittable.
088    this.splitkey = r.getSplitKey();
089    this.splitCell = new KeyValue.KeyOnlyKeyValue(this.splitkey, 0, this.splitkey.length);
090    // Is it top or bottom half?
091    this.top = Reference.isTopFileRegion(r.getFileRegion());
092  }
093
094  protected boolean isTop() {
095    return this.top;
096  }
097
098  @Override
099  protected HFileScanner getScanner(final boolean cacheBlocks, final boolean pread,
100    final boolean isCompaction) {
101    final HFileScanner s = super.getScanner(cacheBlocks, pread, isCompaction);
102    return new HFileScanner() {
103      final HFileScanner delegate = s;
104      public boolean atEnd = false;
105
106      @Override
107      public ExtendedCell getKey() {
108        if (atEnd) {
109          return null;
110        }
111        return delegate.getKey();
112      }
113
114      @Override
115      public ByteBuffer getValue() {
116        if (atEnd) {
117          return null;
118        }
119
120        return delegate.getValue();
121      }
122
123      @Override
124      public ExtendedCell getCell() {
125        if (atEnd) {
126          return null;
127        }
128
129        return delegate.getCell();
130      }
131
132      @Override
133      public boolean next() throws IOException {
134        if (atEnd) return false;
135
136        boolean b = delegate.next();
137        if (!b) {
138          return b;
139        }
140        // constrain the bottom.
141        if (!top) {
142          if (getComparator().compare(splitCell, getKey()) <= 0) {
143            atEnd = true;
144            return false;
145          }
146        }
147        return true;
148      }
149
150      @Override
151      public boolean seekTo() throws IOException {
152        if (top) {
153          int r = this.delegate.seekTo(splitCell);
154          if (r == HConstants.INDEX_KEY_MAGIC) {
155            return true;
156          }
157          if (r < 0) {
158            // midkey is < first key in file
159            return this.delegate.seekTo();
160          }
161          if (r > 0) {
162            return this.delegate.next();
163          }
164          return true;
165        }
166
167        boolean b = delegate.seekTo();
168        if (!b) {
169          return b;
170        }
171        // Check key.
172        return (this.delegate.getReader().getComparator().compare(splitCell, getKey())) > 0;
173      }
174
175      @Override
176      public org.apache.hadoop.hbase.io.hfile.HFile.Reader getReader() {
177        return this.delegate.getReader();
178      }
179
180      @Override
181      public boolean isSeeked() {
182        return this.delegate.isSeeked();
183      }
184
185      @Override
186      public int seekTo(ExtendedCell key) throws IOException {
187        if (top) {
188          if (PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, splitCell) < 0) {
189            return -1;
190          }
191        } else {
192          if (PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, splitCell) >= 0) {
193            // we would place the scanner in the second half.
194            // it might be an error to return false here ever...
195            boolean res = delegate.seekBefore(splitCell);
196            if (!res) {
197              throw new IOException(
198                "Seeking for a key in bottom of file, but key exists in top of file, "
199                  + "failed on seekBefore(midkey)");
200            }
201            return 1;
202          }
203        }
204        return delegate.seekTo(key);
205      }
206
207      @Override
208      public int reseekTo(ExtendedCell key) throws IOException {
209        // This function is identical to the corresponding seekTo function
210        // except that we call reseekTo (and not seekTo) on the delegate.
211        if (top) {
212          if (PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, splitCell) < 0) {
213            return -1;
214          }
215        } else {
216          if (PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, splitCell) >= 0) {
217            // we would place the scanner in the second half.
218            // it might be an error to return false here ever...
219            boolean res = delegate.seekBefore(splitCell);
220            if (!res) {
221              throw new IOException("Seeking for a key in bottom of file, but"
222                + " key exists in top of file, failed on seekBefore(midkey)");
223            }
224            return 1;
225          }
226        }
227        if (atEnd) {
228          // skip the 'reseek' and just return 1.
229          return 1;
230        }
231        return delegate.reseekTo(key);
232      }
233
234      @Override
235      public boolean seekBefore(ExtendedCell key) throws IOException {
236        if (top) {
237          Optional<ExtendedCell> fk = getFirstKey();
238          if (
239            fk.isPresent()
240              && PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, fk.get()) <= 0
241          ) {
242            return false;
243          }
244        } else {
245          // The equals sign isn't strictly necessary just here to be consistent
246          // with seekTo
247          if (PrivateCellUtil.compareKeyIgnoresMvcc(getComparator(), key, splitCell) >= 0) {
248            boolean ret = this.delegate.seekBefore(splitCell);
249            if (ret) {
250              atEnd = false;
251            }
252            return ret;
253          }
254        }
255        boolean ret = this.delegate.seekBefore(key);
256        if (ret) {
257          atEnd = false;
258        }
259        return ret;
260      }
261
262      @Override
263      public ExtendedCell getNextIndexedKey() {
264        return null;
265      }
266
267      @Override
268      public void close() {
269        this.delegate.close();
270      }
271
272      @Override
273      public void shipped() throws IOException {
274        this.delegate.shipped();
275      }
276
277      @Override
278      public void recordBlockSize(IntConsumer blockSizeConsumer) {
279        this.delegate.recordBlockSize(blockSizeConsumer);
280      }
281    };
282  }
283
284  @Override
285  public boolean passesKeyRangeFilter(Scan scan) {
286    return true;
287  }
288
289  @Override
290  public Optional<ExtendedCell> getLastKey() {
291    if (top) {
292      return super.getLastKey();
293    }
294    // Get a scanner that caches the block and that uses pread.
295    HFileScanner scanner = getScanner(true, true, false);
296    try {
297      if (scanner.seekBefore(this.splitCell)) {
298        return Optional.ofNullable(scanner.getKey());
299      }
300    } catch (IOException e) {
301      LOG.warn("Failed seekBefore " + Bytes.toStringBinary(this.splitkey), e);
302    } finally {
303      if (scanner != null) {
304        scanner.close();
305      }
306    }
307    return Optional.empty();
308  }
309
310  @Override
311  public Optional<ExtendedCell> midKey() throws IOException {
312    // Returns null to indicate file is not splitable.
313    return Optional.empty();
314  }
315
316  @Override
317  public Optional<ExtendedCell> getFirstKey() {
318    if (!firstKeySeeked) {
319      HFileScanner scanner = getScanner(true, true, false);
320      try {
321        if (scanner.seekTo()) {
322          this.firstKey = Optional.ofNullable(scanner.getKey());
323        }
324        firstKeySeeked = true;
325      } catch (IOException e) {
326        LOG.warn("Failed seekTo first KV in the file", e);
327      } finally {
328        if (scanner != null) {
329          scanner.close();
330        }
331      }
332    }
333    return this.firstKey;
334  }
335
336  @Override
337  public long getEntries() {
338    // Estimate the number of entries as half the original file; this may be wildly inaccurate.
339    return super.getEntries() / 2;
340  }
341
342  @Override
343  public long getFilterEntries() {
344    // Estimate the number of entries as half the original file; this may be wildly inaccurate.
345    return super.getFilterEntries() / 2;
346  }
347
348  /**
349   * Overrides close method to handle cache evictions for the referred file. If evictionOnClose is
350   * true, we will seek to the block containing the splitCell and evict all blocks from offset 0 up
351   * to that block offset if this is a bottom half reader, or the from the split block offset up to
352   * the end of the file if this is a top half reader.
353   * @param evictOnClose true if it should evict the file blocks from the cache.
354   */
355  @Override
356  public void close(boolean evictOnClose) throws IOException {
357    if (closed.compareAndSet(false, true)) {
358      if (evictOnClose) {
359        final HFileReaderImpl.HFileScannerImpl s =
360          (HFileReaderImpl.HFileScannerImpl) super.getScanner(false, true, false);
361        final String reference = this.reader.getHFileInfo().getHFileContext().getHFileName();
362        final String referred = StoreFileInfo.getReferredToRegionAndFile(reference).getSecond();
363        s.seekTo(splitCell);
364        if (s.getCurBlock() != null) {
365          long offset = s.getCurBlock().getOffset();
366          LOG.trace("Seeking to split cell in reader: {} for file: {} top: {}, split offset: {}",
367            this, reference, top, offset);
368          ((HFileReaderImpl) reader).getCacheConf().getBlockCache().ifPresent(cache -> {
369            int numEvictedReferred = top
370              ? cache.evictBlocksRangeByHfileName(referred, offset, Long.MAX_VALUE)
371              : cache.evictBlocksRangeByHfileName(referred, 0, offset);
372            int numEvictedReference = cache.evictBlocksByHfileName(reference);
373            LOG.trace(
374              "Closing reference: {}; referred file: {}; was top? {}; evicted for referred: {};"
375                + "evicted for reference: {}",
376              reference, referred, top, numEvictedReferred, numEvictedReference);
377          });
378        }
379        s.close();
380        reader.close(false);
381      } else {
382        reader.close(evictOnClose);
383      }
384    }
385  }
386}