package com.ctriposs.sdb.table;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.xerial.snappy.Snappy;
import com.ctriposs.sdb.utils.MMFUtil;
import com.google.common.base.Preconditions;
/**
* In memory hashmap backed by memory mapped WAL(Write Ahead Log)
*
* @author bulldog
*
*/
public class HashMapTable extends AbstractMapTable {
private AtomicBoolean immutable = new AtomicBoolean(true);
private ConcurrentHashMap<ByteArrayWrapper, InMemIndex> hashMap;
protected ThreadLocalByteBuffer localDataMappedByteBuffer;
protected ThreadLocalByteBuffer localIndexMappedByteBuffer;
private boolean compressionEnabled = true;
// Create new
public HashMapTable(String dir, int level, long createdTime)
throws IOException {
super(dir, level, createdTime);
mapIndexAndDataFiles();
initToAppendIndexAndOffset();
}
public HashMapTable(String dir, short shard, int level, long createdTime)
throws IOException {
super(dir, shard, level, createdTime);
mapIndexAndDataFiles();
initToAppendIndexAndOffset();
}
// Load existing
public HashMapTable(String dir, String fileName)
throws IOException {
super(dir, fileName);
mapIndexAndDataFiles();
initToAppendIndexAndOffset();
}
public void setCompressionEnabled(boolean enabled) {
this.compressionEnabled = enabled;
}
private void mapIndexAndDataFiles() throws IOException {
MappedByteBuffer indexMappedByteBuffer = this.indexChannel.map(MapMode.READ_WRITE, 0, this.indexChannel.size());
localIndexMappedByteBuffer = new ThreadLocalByteBuffer(indexMappedByteBuffer);
MappedByteBuffer dataMappedByteBuffer = this.dataChannel.map(MapMode.READ_WRITE, 0, this.dataChannel.size());
localDataMappedByteBuffer = new ThreadLocalByteBuffer(dataMappedByteBuffer);
}
public Set<Map.Entry<ByteArrayWrapper, InMemIndex>> getEntrySet() {
ensureNotClosed();
return this.hashMap.entrySet();
}
private void initToAppendIndexAndOffset() throws IOException {
this.hashMap = new ConcurrentHashMap<ByteArrayWrapper, InMemIndex>(INIT_INDEX_ITEMS_PER_TABLE);
toAppendIndex = new AtomicInteger(0);
toAppendDataFileOffset = new AtomicLong(0);
int index = 0;
MMFMapEntryImpl mapEntry = new MMFMapEntryImpl(index, this.localIndexMappedByteBuffer.get(), this.localDataMappedByteBuffer.get());
while(mapEntry.isInUse()) {
toAppendIndex.incrementAndGet();
long nextOffset = mapEntry.getItemOffsetInDataFile() + mapEntry.getKeyLength() + mapEntry.getValueLength();
toAppendDataFileOffset.set(nextOffset);
InMemIndex inMemIndex = new InMemIndex(index);
// populate in memory skip list
hashMap.put(new ByteArrayWrapper(mapEntry.getKey()), inMemIndex);
index++;
mapEntry = new MMFMapEntryImpl(index, this.localIndexMappedByteBuffer.get(), this.localDataMappedByteBuffer.get());
}
}
// for testing
public IMapEntry appendNew(byte[] key, byte[] value, long timeToLive, long createdTime) throws IOException {
Preconditions.checkArgument(key != null && key.length > 0, "Key is empty");
Preconditions.checkArgument(value != null && value.length > 0, "value is empty");
return this.appendNew(key, Arrays.hashCode(key), value, timeToLive, createdTime, false, false);
}
private IMapEntry appendTombstone(byte[] key) throws IOException {
Preconditions.checkArgument(key != null && key.length > 0, "Key is empty");
return this.appendNew(key, Arrays.hashCode(key), new byte[] {0}, NO_TIMEOUT, System.currentTimeMillis(), true, false);
}
private IMapEntry appendNewCompressed(byte[] key, byte[] value, long timeToLive, long createdTime) throws IOException {
Preconditions.checkArgument(key != null && key.length > 0, "Key is empty");
Preconditions.checkArgument(value != null && value.length > 0, "value is empty");
return this.appendNew(key, Arrays.hashCode(key), value, timeToLive, createdTime, false, true);
}
private IMapEntry appendNew(byte[] key, int keyHash, byte[] value, long timeToLive, long createdTime, boolean markDelete, boolean compressed) throws IOException {
ensureNotClosed();
long tempToAppendIndex;
long tempToAppendDataFileOffset;
appendLock.lock();
try {
if (toAppendIndex.get() == INIT_INDEX_ITEMS_PER_TABLE) { // index overflow
return null;
}
int dataLength = key.length + value.length;
if (toAppendDataFileOffset.get() + dataLength > INIT_DATA_FILE_SIZE) { // data overflow
return null;
}
tempToAppendIndex = toAppendIndex.get();
tempToAppendDataFileOffset = toAppendDataFileOffset.get();
// commit/update offset & index
toAppendDataFileOffset.addAndGet(dataLength);
toAppendIndex.incrementAndGet();
}
finally {
appendLock.unlock();
}
// write index metadata
ByteBuffer tempIndexBuf = ByteBuffer.allocate(INDEX_ITEM_LENGTH);
tempIndexBuf.putLong(IMapEntry.INDEX_ITEM_IN_DATA_FILE_OFFSET_OFFSET, tempToAppendDataFileOffset);
tempIndexBuf.putInt(IMapEntry.INDEX_ITEM_KEY_LENGTH_OFFSET, key.length);
tempIndexBuf.putInt(IMapEntry.INDEX_ITEM_VALUE_LENGTH_OFFSET, value.length);
tempIndexBuf.putLong(IMapEntry.INDEX_ITEM_TIME_TO_LIVE_OFFSET, timeToLive);
tempIndexBuf.putLong(IMapEntry.INDEX_ITEM_CREATED_TIME_OFFSET, createdTime);
tempIndexBuf.putInt(IMapEntry.INDEX_ITEM_KEY_HASH_CODE_OFFSET, keyHash);
byte status = 1; // mark in use
if (markDelete) {
status = (byte) (status + 2); // binary 11
}
if (compressed && !markDelete) {
status = (byte) (status + 4);
}
tempIndexBuf.put(IMapEntry.INDEX_ITEM_STATUS, status); // mark in use
int offsetInIndexFile = INDEX_ITEM_LENGTH * (int)tempToAppendIndex;
ByteBuffer localIndexBuffer = this.localIndexMappedByteBuffer.get();
localIndexBuffer.position(offsetInIndexFile);
//indexBuf.rewind();
localIndexBuffer.put(tempIndexBuf);
// write key/value
ByteBuffer localDataBuffer = this.localDataMappedByteBuffer.get();
localDataBuffer.position((int)tempToAppendDataFileOffset);
localDataBuffer.put(ByteBuffer.wrap(key));
localDataBuffer.position((int)tempToAppendDataFileOffset + key.length);
localDataBuffer.put(ByteBuffer.wrap(value));
this.hashMap.put(new ByteArrayWrapper(key), new InMemIndex((int)tempToAppendIndex));
return new MMFMapEntryImpl((int)tempToAppendIndex, localIndexBuffer, localDataBuffer);
}
@Override
public IMapEntry getMapEntry(int index) {
ensureNotClosed();
Preconditions.checkArgument(index >= 0, "index (%s) must be equal to or greater than 0", index);
Preconditions.checkArgument(!isEmpty(), "Can't get map entry since the map is empty");
return new MMFMapEntryImpl(index, this.localIndexMappedByteBuffer.get(), this.localDataMappedByteBuffer.get());
}
@Override
public GetResult get(byte[] key) throws IOException {
ensureNotClosed();
Preconditions.checkArgument(key != null && key.length > 0, "Key is empty");
GetResult result = new GetResult();
InMemIndex inMemIndex = this.hashMap.get(new ByteArrayWrapper(key));
if (inMemIndex == null) return result;
IMapEntry mapEntry = this.getMapEntry(inMemIndex.getIndex());
if (mapEntry.isCompressed()) {
result.setValue(Snappy.uncompress(mapEntry.getValue()));
} else {
result.setValue(mapEntry.getValue());
}
if (mapEntry.isDeleted()) {
result.setDeleted(true);
return result;
}
if (mapEntry.isExpired()) {
result.setExpired(true);
return result;
}
result.setLevel(this.getLevel());
result.setTimeToLive(mapEntry.getTimeToLive());
result.setCreatedTime(mapEntry.getCreatedTime());
return result;
}
public void markImmutable(boolean immutable) {
this.immutable.set(immutable);
}
public boolean isImmutable() {
return this.immutable.get();
}
public boolean put(byte[] key, byte[] value, long timeToLive, long createdTime, boolean isDelete) throws IOException {
ensureNotClosed();
Preconditions.checkArgument(key != null && key.length > 0, "Key is empty");
Preconditions.checkArgument(value != null && value.length > 0, "value is empty");
IMapEntry mapEntry = null;
if (isDelete) {
// make a tombstone
mapEntry = this.appendTombstone(key);
} else {
mapEntry = this.compressionEnabled ?
this.appendNewCompressed(key, Snappy.compress(value), timeToLive, createdTime) : this.appendNew(key, value, timeToLive, createdTime);
}
if (mapEntry == null) { // no space
return false;
}
return true;
}
public void put(byte[] key, byte[] value, long timeToLive, long createdTime) throws IOException {
this.put(key, value, timeToLive, createdTime, false);
}
public void delete(byte[] key) throws IOException {
this.appendTombstone(key);
}
public int getRealSize() {
return this.hashMap.size();
}
@Override
public void close() throws IOException {
if (this.localIndexMappedByteBuffer == null) return;
if (this.localDataMappedByteBuffer == null) return;
MMFUtil.unmap((MappedByteBuffer)this.localIndexMappedByteBuffer.getSourceBuffer());
this.localIndexMappedByteBuffer = null;
MMFUtil.unmap((MappedByteBuffer)this.localDataMappedByteBuffer.getSourceBuffer());
this.localDataMappedByteBuffer = null;
super.close();
}
}