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.hfile.bucket;
019
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.util.Comparator;
023import java.util.concurrent.atomic.AtomicBoolean;
024import java.util.concurrent.locks.ReentrantReadWriteLock;
025import java.util.function.Function;
026import org.apache.hadoop.hbase.io.ByteBuffAllocator;
027import org.apache.hadoop.hbase.io.ByteBuffAllocator.Recycler;
028import org.apache.hadoop.hbase.io.hfile.BlockPriority;
029import org.apache.hadoop.hbase.io.hfile.Cacheable;
030import org.apache.hadoop.hbase.io.hfile.CacheableDeserializer;
031import org.apache.hadoop.hbase.io.hfile.CacheableDeserializerIdManager;
032import org.apache.hadoop.hbase.nio.ByteBuff;
033import org.apache.hadoop.hbase.nio.HBaseReferenceCounted;
034import org.apache.hadoop.hbase.nio.RefCnt;
035import org.apache.hadoop.hbase.util.IdReadWriteLock;
036import org.apache.yetus.audience.InterfaceAudience;
037
038/**
039 * Item in cache. We expect this to be where most memory goes. Java uses 8 bytes just for object
040 * headers; after this, we want to use as little as possible - so we only use 8 bytes, but in order
041 * to do so we end up messing around with all this Java casting stuff. Offset stored as 5 bytes that
042 * make up the long. Doubt we'll see devices this big for ages. Offsets are divided by 256. So 5
043 * bytes gives us 256TB or so.
044 */
045@InterfaceAudience.Private
046public class BucketEntry implements HBaseReferenceCounted {
047  // access counter comparator, descending order
048  static final Comparator<BucketEntry> COMPARATOR =
049    Comparator.comparingLong(BucketEntry::getAccessCounter).reversed();
050
051  private int offsetBase;
052  private int length;
053
054  private int onDiskSizeWithHeader;
055  private byte offset1;
056
057  /**
058   * The index of the deserializer that can deserialize this BucketEntry content. See
059   * {@link CacheableDeserializerIdManager} for hosting of index to serializers.
060   */
061  byte deserializerIndex;
062
063  private volatile long accessCounter;
064  private BlockPriority priority;
065
066  /**
067   * <pre>
068   * The RefCnt means how many paths are referring the {@link BucketEntry}, there are two cases:
069   * 1.If the {@link IOEngine#usesSharedMemory()} is false(eg.{@link FileIOEngine}),the refCnt is
070   *   always 1 until this {@link BucketEntry} is evicted from {@link BucketCache#backingMap}.Even
071   *   if the corresponding {@link HFileBlock} is referenced by RPC reading, the refCnt should not
072   *   increase.
073   *
074   * 2.If the {@link IOEngine#usesSharedMemory()} is true(eg.{@link ByteBufferIOEngine}),each RPC
075   *   reading path is considering as one path, the {@link BucketCache#backingMap} reference is
076   *   also considered a path. NOTICE that if two read RPC path hit the same {@link BucketEntry},
077   *   then the {@link HFileBlock}s the two RPC referred will share the same refCnt instance with
078   *   the {@link BucketEntry},so the refCnt will increase or decrease as the following:
079   *   (1) when writerThread flush the block into IOEngine and add the bucketEntry into backingMap,
080   *       the refCnt ++;
081   *   (2) If BucketCache evict the block and move the bucketEntry out of backingMap, the refCnt--;
082   *       it usually happen when HFile is closing or someone call the clearBucketCache by force.
083   *   (3) The read RPC path start to refer the block which is backend by the memory area in
084   *       bucketEntry, then refCnt ++ ;
085   *   (4) The read RPC patch shipped the response, and release the block. then refCnt--;
086   *    Once the refCnt decrease to zero, then the {@link BucketAllocator} will free the block area.
087   * </pre>
088   */
089  private final RefCnt refCnt;
090  final AtomicBoolean markedAsEvicted;
091  final ByteBuffAllocator allocator;
092
093  /**
094   * Time this block was cached. Presumes we are created just before we are added to the cache.
095   */
096  private long cachedTime = System.nanoTime();
097
098  /**
099   * @param createRecycler used to free this {@link BucketEntry} when {@link BucketEntry#refCnt}
100   *                       becoming 0. NOTICE that {@link ByteBuffAllocator#NONE} could only be used
101   *                       for test.
102   */
103  BucketEntry(long offset, int length, int onDiskSizeWithHeader, long accessCounter,
104    boolean inMemory, Function<BucketEntry, Recycler> createRecycler, ByteBuffAllocator allocator) {
105    this(offset, length, onDiskSizeWithHeader, accessCounter, System.nanoTime(),
106      inMemory ? BlockPriority.MEMORY : BlockPriority.SINGLE, createRecycler, allocator);
107  }
108
109  /**
110   * @param priority the priority of the entry, e.g. the one recovered from a persisted backing map
111   */
112  BucketEntry(long offset, int length, int onDiskSizeWithHeader, long accessCounter,
113    long cachedTime, BlockPriority priority, Function<BucketEntry, Recycler> createRecycler,
114    ByteBuffAllocator allocator) {
115    if (createRecycler == null) {
116      throw new IllegalArgumentException("createRecycler could not be null!");
117    }
118    setOffset(offset);
119    this.length = length;
120    this.onDiskSizeWithHeader = onDiskSizeWithHeader;
121    this.accessCounter = accessCounter;
122    this.cachedTime = cachedTime;
123    this.priority = priority;
124    this.refCnt = RefCnt.create(createRecycler.apply(this));
125    this.markedAsEvicted = new AtomicBoolean(false);
126    this.allocator = allocator;
127  }
128
129  long offset() {
130    // Java has no unsigned numbers, so this needs the L cast otherwise it will be sign extended
131    // as a negative number.
132    long o = ((long) offsetBase) & 0xFFFFFFFFL;
133    // The 0xFF here does not need the L cast because it is treated as a positive int.
134    o += (((long) (offset1)) & 0xFF) << 32;
135    return o << 8;
136  }
137
138  private void setOffset(long value) {
139    assert (value & 0xFF) == 0;
140    value >>= 8;
141    offsetBase = (int) value;
142    offset1 = (byte) (value >> 32);
143  }
144
145  public int getLength() {
146    return length;
147  }
148
149  CacheableDeserializer<Cacheable> deserializerReference() {
150    return CacheableDeserializerIdManager.getDeserializer(deserializerIndex);
151  }
152
153  void setDeserializerReference(CacheableDeserializer<Cacheable> deserializer) {
154    this.deserializerIndex = (byte) deserializer.getDeserializerIdentifier();
155  }
156
157  long getAccessCounter() {
158    return accessCounter;
159  }
160
161  /**
162   * Block has been accessed. Update its local access counter.
163   */
164  void access(long accessCounter) {
165    this.accessCounter = accessCounter;
166    if (this.priority == BlockPriority.SINGLE) {
167      this.priority = BlockPriority.MULTI;
168    }
169  }
170
171  public BlockPriority getPriority() {
172    return this.priority;
173  }
174
175  public long getCachedTime() {
176    return cachedTime;
177  }
178
179  public int getOnDiskSizeWithHeader() {
180    return onDiskSizeWithHeader;
181  }
182
183  /**
184   * The {@link BucketCache} will try to release its reference to this BucketEntry many times. we
185   * must make sure the idempotent, otherwise it'll decrease the RPC's reference count in advance,
186   * then for RPC memory leak happen.
187   * @return true if we deallocate this entry successfully.
188   */
189  boolean markAsEvicted() {
190    if (markedAsEvicted.compareAndSet(false, true)) {
191      return this.release();
192    }
193    return false;
194  }
195
196  /**
197   * Check whether have some RPC patch referring this block.<br/>
198   * For {@link IOEngine#usesSharedMemory()} is true(eg.{@link ByteBufferIOEngine}), there're two
199   * case: <br>
200   * 1. If current refCnt is greater than 1, there must be at least one referring RPC path; <br>
201   * 2. If current refCnt is equal to 1 and the markedAtEvicted is true, the it means backingMap has
202   * released its reference, the remaining reference can only be from RPC path. <br>
203   * We use this check to decide whether we can free the block area: when cached size exceed the
204   * acceptable size, our eviction policy will choose those stale blocks without any RPC reference
205   * and the RPC referred block will be excluded. <br/>
206   * <br/>
207   * For {@link IOEngine#usesSharedMemory()} is false(eg.{@link FileIOEngine}),
208   * {@link BucketEntry#refCnt} is always 1 until it is evicted from {@link BucketCache#backingMap},
209   * so {@link BucketEntry#isRpcRef()} is always return false.
210   * @return true to indicate there're some RPC referring the block.
211   */
212  boolean isRpcRef() {
213    boolean evicted = markedAsEvicted.get();
214    return this.refCnt() > 1 || (evicted && refCnt() == 1);
215  }
216
217  Cacheable wrapAsCacheable(ByteBuffer[] buffers) throws IOException {
218    return wrapAsCacheable(ByteBuff.wrap(buffers, this.refCnt));
219  }
220
221  Cacheable wrapAsCacheable(ByteBuff buf) throws IOException {
222    return this.deserializerReference().deserialize(buf, allocator);
223  }
224
225  interface BucketEntryHandler<T> {
226    T handle();
227  }
228
229  <T> T withWriteLock(IdReadWriteLock<Long> offsetLock, BucketEntryHandler<T> handler) {
230    ReentrantReadWriteLock lock = offsetLock.getLock(this.offset());
231    try {
232      lock.writeLock().lock();
233      return handler.handle();
234    } finally {
235      lock.writeLock().unlock();
236    }
237  }
238
239  @Override
240  public int refCnt() {
241    return this.refCnt.refCnt();
242  }
243
244  @Override
245  public BucketEntry retain() {
246    refCnt.retain();
247    return this;
248  }
249
250  /**
251   * We've three cases to release refCnt now: <br>
252   * 1. BucketCache#evictBlock, it will release the backingMap's reference by force because we're
253   * closing file or clear the bucket cache or some corruption happen. when all rpc references gone,
254   * then free the area in bucketAllocator. <br>
255   * 2. BucketCache#returnBlock . when rpc shipped, we'll release the block, only when backingMap
256   * also release its refCnt (case.1 will do this) and no other rpc reference, then it will free the
257   * area in bucketAllocator. <br>
258   * 3.evict those block without any rpc reference if cache size exceeded. we'll only free those
259   * blocks with zero rpc reference count.
260   * @return true to indicate we've decreased to zero and do the de-allocation.
261   */
262  @Override
263  public boolean release() {
264    return refCnt.release();
265  }
266}