/**
* 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.regionserver.wal;
import java.util.HashMap;
import org.apache.hadoop.hbase.util.Bytes;
import 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.
*/
public class LRUDictionary implements Dictionary {
private final BidirectionalLRUMap backingStore = new BidirectionalLRUMap();
@Override
public byte[] getEntry(short idx) {
return backingStore.get(idx);
}
@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) {
if (length <= 0) return NOT_IN_DICTIONARY;
return backingStore.put(data, offset, length);
}
@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 {
static final int MAX_SIZE = Short.MAX_VALUE;
private int currSize = 0;
// Head and tail of the LRU list.
private Node head;
private Node tail;
private HashMap<Node, Short> nodeToIndex = new HashMap<Node, Short>();
private Node[] indexToNode = new Node[MAX_SIZE];
public BidirectionalLRUMap() {
for (int i = 0; i < MAX_SIZE; i++) {
indexToNode[i] = new Node();
}
}
private short put(byte[] array, int offset, int length) {
// 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);
if (currSize < MAX_SIZE) {
// There is space to add without evicting.
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 Node();
comparisonNode.setContents(array, 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].container;
}
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() {
currSize = 0;
nodeToIndex.clear();
tail = null;
head = null;
for (Node n : indexToNode) {
n.container = null;
}
for (int i = 0; i < MAX_SIZE; i++) {
indexToNode[i].next = null;
indexToNode[i].prev = null;
}
}
private static class Node {
byte[] container;
int offset;
int length;
Node next; // link towards the tail
Node prev; // link towards the head
public Node() {
}
private void setContents(byte[] container, int offset, int length) {
this.container = container;
this.offset = offset;
this.length = length;
}
@Override
public int hashCode() {
return Bytes.hashCode(container, offset, length);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Node)) {
return false;
}
Node casted = (Node) other;
return Bytes.equals(container, offset, length, casted.container,
casted.offset, casted.length);
}
}
}
}