| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.hadoop.hbase.io.util; |
| |
| import java.nio.ByteBuffer; |
| import java.util.HashMap; |
| |
| import org.apache.hadoop.hbase.util.ByteBufferUtils; |
| import org.apache.hadoop.hbase.util.Bytes; |
| import org.apache.yetus.audience.InterfaceAudience; |
| |
| import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; |
| |
| /** |
| * WALDictionary using an LRU eviction algorithm. Uses a linked list running |
| * through a hashtable. Currently has max of 2^15 entries. Will start |
| * evicting if exceeds this number The maximum memory we expect this dictionary |
| * to take in the worst case is about: |
| * <code>(2 ^ 15) * 5 (Regionname, Row key, CF, Column qual, table) * 100 bytes |
| * (these are some big names) = ~16MB</code>. |
| * If you want to get silly, even at 1kb entries, it maxes out at 160 megabytes. |
| */ |
| @InterfaceAudience.Private |
| public class LRUDictionary implements Dictionary { |
| |
| BidirectionalLRUMap backingStore; |
| @Override |
| public byte[] getEntry(short idx) { |
| return backingStore.get(idx); |
| } |
| |
| @Override |
| public void init(int initialSize) { |
| backingStore = new BidirectionalLRUMap(initialSize); |
| } |
| @Override |
| public short findEntry(byte[] data, int offset, int length) { |
| short ret = backingStore.findIdx(data, offset, length); |
| if (ret == NOT_IN_DICTIONARY) { |
| addEntry(data, offset, length); |
| } |
| return ret; |
| } |
| |
| @Override |
| public short addEntry(byte[] data, int offset, int length) { |
| return addEntryInternal(data, offset, length, true); |
| } |
| |
| private short addEntryInternal(byte[] data, int offset, int length, boolean copy) { |
| if (length <= 0) return NOT_IN_DICTIONARY; |
| return backingStore.put(data, offset, length, copy); |
| } |
| |
| @Override |
| public void clear() { |
| backingStore.clear(); |
| } |
| |
| /* |
| * Internal class used to implement LRU eviction and dual lookup (by key and |
| * value). |
| * |
| * This is not thread safe. Don't use in multi-threaded applications. |
| */ |
| static class BidirectionalLRUMap { |
| private int currSize = 0; |
| |
| // Head and tail of the LRU list. |
| private Node head; |
| private Node tail; |
| |
| private HashMap<Node, Short> nodeToIndex = new HashMap<>(); |
| private Node[] indexToNode; |
| private int initSize = 0; |
| |
| public BidirectionalLRUMap(int initialSize) { |
| initSize = initialSize; |
| indexToNode = new Node[initialSize]; |
| } |
| |
| private short put(byte[] array, int offset, int length, boolean copy) { |
| if (copy) { |
| // We copy the bytes we want, otherwise we might be holding references to |
| // massive arrays in our dictionary (or those arrays might change) |
| byte[] stored = new byte[length]; |
| Bytes.putBytes(stored, 0, array, offset, length); |
| return putInternal(stored); |
| } else { |
| return putInternal(array); |
| } |
| } |
| |
| private short putInternal(byte[] stored) { |
| if (currSize < initSize) { |
| // There is space to add without evicting. |
| if (indexToNode[currSize] == null) { |
| indexToNode[currSize] = new ByteArrayBackedNode(); |
| } |
| indexToNode[currSize].setContents(stored, 0, stored.length); |
| setHead(indexToNode[currSize]); |
| short ret = (short) currSize++; |
| nodeToIndex.put(indexToNode[ret], ret); |
| return ret; |
| } else { |
| short s = nodeToIndex.remove(tail); |
| tail.setContents(stored, 0, stored.length); |
| // we need to rehash this. |
| nodeToIndex.put(tail, s); |
| moveToHead(tail); |
| return s; |
| } |
| } |
| |
| private short findIdx(byte[] array, int offset, int length) { |
| Short s; |
| final Node comparisonNode = new ByteArrayBackedNode(); |
| comparisonNode.setContents(array, offset, length); |
| if ((s = nodeToIndex.get(comparisonNode)) != null) { |
| moveToHead(indexToNode[s]); |
| return s; |
| } else { |
| return -1; |
| } |
| } |
| |
| private short findIdx(ByteBuffer buf, int offset, int length) { |
| Short s; |
| final ByteBufferBackedNode comparisonNode = new ByteBufferBackedNode(); |
| comparisonNode.setContents(buf, offset, length); |
| if ((s = nodeToIndex.get(comparisonNode)) != null) { |
| moveToHead(indexToNode[s]); |
| return s; |
| } else { |
| return -1; |
| } |
| } |
| |
| private byte[] get(short idx) { |
| Preconditions.checkElementIndex(idx, currSize); |
| moveToHead(indexToNode[idx]); |
| return indexToNode[idx].getContents(); |
| } |
| |
| private void moveToHead(Node n) { |
| if (head == n) { |
| // no-op -- it's already the head. |
| return; |
| } |
| // At this point we definitely have prev, since it's not the head. |
| assert n.prev != null; |
| // Unlink prev. |
| n.prev.next = n.next; |
| |
| // Unlink next |
| if (n.next != null) { |
| n.next.prev = n.prev; |
| } else { |
| assert n == tail; |
| tail = n.prev; |
| } |
| // Node is now removed from the list. Re-add it at the head. |
| setHead(n); |
| } |
| |
| private void setHead(Node n) { |
| // assume it's already unlinked from the list at this point. |
| n.prev = null; |
| n.next = head; |
| if (head != null) { |
| assert head.prev == null; |
| head.prev = n; |
| } |
| |
| head = n; |
| |
| // First entry |
| if (tail == null) { |
| tail = n; |
| } |
| } |
| |
| private void clear() { |
| for (int i = 0; i < currSize; i++) { |
| indexToNode[i].next = null; |
| indexToNode[i].prev = null; |
| indexToNode[i].resetContents(); |
| } |
| currSize = 0; |
| nodeToIndex.clear(); |
| tail = null; |
| head = null; |
| } |
| |
| private static abstract class Node { |
| int offset; |
| int length; |
| Node next; // link towards the tail |
| Node prev; // link towards the head |
| abstract void setContents(byte[] container, int offset, int length); |
| abstract byte[] getContents(); |
| abstract void resetContents(); |
| } |
| // The actual contents of the LRUDictionary are of ByteArrayBackedNode type |
| private static class ByteArrayBackedNode extends Node { |
| private byte[] container; |
| |
| @Override |
| void setContents(byte[] container, int offset, int length) { |
| this.container = container; |
| this.offset = offset; |
| this.length = length; |
| } |
| |
| @Override |
| byte[] getContents() { |
| return this.container; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Bytes.hashCode(container, offset, length); |
| } |
| |
| @Override |
| void resetContents() { |
| this.container = null; |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (!(other instanceof Node)) { |
| return false; |
| } |
| |
| Node casted = (Node) other; |
| return Bytes.equals(container, offset, length, casted.getContents(), |
| casted.offset, casted.length); |
| } |
| } |
| |
| // Currently only used for finding the index and hence this node acts |
| // as a temporary holder to look up in the indexToNode map |
| // which is formed by ByteArrayBackedNode |
| private static class ByteBufferBackedNode extends Node { |
| private ByteBuffer container; |
| |
| public ByteBufferBackedNode() { |
| } |
| |
| @Override |
| void setContents(byte[] container, int offset, int length) { |
| this.container = ByteBuffer.wrap(container); |
| this.offset = offset; |
| this.length = length; |
| } |
| |
| void setContents(ByteBuffer container, int offset, int length) { |
| this.container = container; |
| this.offset = offset; |
| this.length = length; |
| } |
| |
| @Override |
| void resetContents() { |
| this.container = null; |
| } |
| |
| @Override |
| byte[] getContents() { |
| // This ideally should not be called |
| byte[] copy = new byte[this.length]; |
| ByteBufferUtils.copyFromBufferToArray(copy, (ByteBuffer) this.container, this.offset, 0, |
| length); |
| return copy; |
| } |
| |
| @Override |
| public int hashCode() { |
| return ByteBufferUtils.hashCode(container, offset, length); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (!(other instanceof Node)) { |
| return false; |
| } |
| // This was done to avoid findbugs comment |
| Node casted = (Node) other; |
| // The other side should be a byte array backed node only as we add only |
| // ByteArrayBackedNode to the indexToNode map. |
| return ByteBufferUtils.equals(this.container, offset, length, |
| casted.getContents(), casted.offset, casted.length); |
| } |
| } |
| } |
| |
| @Override |
| public short findEntry(ByteBuffer data, int offset, int length) { |
| short ret = backingStore.findIdx(data, offset, length); |
| if (ret == NOT_IN_DICTIONARY) { |
| byte[] copy = new byte[length]; |
| ByteBufferUtils.copyFromBufferToArray(copy, data, offset, 0, length); |
| addEntryInternal(copy, 0, length, false); |
| } |
| return ret; |
| } |
| } |