/**
* Copyright 2013-2015 Seagate Technology LLC.
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0. If a copy of the MPL was not
* distributed with this file, You can obtain one at
* https://mozilla.org/MP:/2.0/.
*
* This program is distributed in the hope that it will be useful,
* but is provided AS-IS, WITHOUT ANY WARRANTY; including without
* the implied warranty of MERCHANTABILITY, NON-INFRINGEMENT or
* FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public
* License for more details.
*
* See www.openkinetic.org for more project information
*/
package com.seagate.kinetic.simulator.persist.leveldb;
import static org.fusesource.leveldbjni.JniDBFactory.factory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import kinetic.simulator.SimulatorConfiguration;
import org.iq80.leveldb.DB;
import org.iq80.leveldb.DBIterator;
import org.iq80.leveldb.Options;
import org.iq80.leveldb.WriteBatch;
import org.iq80.leveldb.WriteOptions;
import com.google.protobuf.ByteString;
import com.seagate.kinetic.simulator.internal.KVStoreException;
import com.seagate.kinetic.simulator.internal.KVStoreNotFound;
import com.seagate.kinetic.simulator.internal.KVStoreVersionMismatch;
import com.seagate.kinetic.simulator.internal.SimulatorEngine;
import com.seagate.kinetic.simulator.persist.BatchOperation;
import com.seagate.kinetic.simulator.persist.KVKey;
import com.seagate.kinetic.simulator.persist.KVValue;
import com.seagate.kinetic.simulator.persist.PersistOption;
import com.seagate.kinetic.simulator.persist.Store;
/**
* LevelDb store for Kinetic simulator.
*
* @author chiaming
*
*/
public class LevelDbStore implements Store<ByteString, ByteString, KVValue> {
private final static java.util.logging.Logger logger = Logger
.getLogger(LevelDbStore.class.getName());
// level db instance
private DB db = null;
// level db file
private String dbFile = null;
// simulator config
private SimulatorConfiguration config = null;
// sync write option
private static final WriteOptions SYNC_WRITE_OPTION = new WriteOptions()
.sync(true);
// async write option
private static final WriteOptions asyncWriteOption = new WriteOptions()
.sync(false);
private String persistFolder = null;
// default no-arg constructor
public LevelDbStore() {
;
}
@Override
public void init(SimulatorConfiguration config) {
this.config = config;
String defaultHome = System.getProperty("user.home") + File.separator
+ "kinetic";
String kineticHome = config.getProperty(
SimulatorConfiguration.KINETIC_HOME, defaultHome);
File lchome = new File(kineticHome);
if (lchome.exists() == false) {
boolean created = lchome.mkdir();
logger.info("create kinetic home folder: " + kineticHome
+ ", created=" + created);
}
// calculate persist folder
persistFolder = kineticHome
+ File.separator
+ config.getProperty(SimulatorConfiguration.PERSIST_HOME,
"leveldb");
File f = new File(persistFolder);
logger.info("Database exists: " + f.exists() + ", persist folder ="
+ persistFolder);
if (f.exists() == false) {
boolean created = f.mkdir();
logger.info("create persist folder: " + persistFolder
+ ", created=" + created);
}
// db file
dbFile = persistFolder + "/leveldb.ldb";
// construct new options
Options options = new Options();
// kinetic comparator
KineticComparator comparator = new KineticComparator();
// set my own comparator
options.comparator(comparator);
// use 64m cache
options.cacheSize(64 * 1048576);
// create if not there
options.createIfMissing(true);
// options.blockSize(64 * 1024 * 1024);
// options.maxOpenFiles(1000);
options.verifyChecksums(true);
try {
// open db file
db = factory.open(new File(dbFile), options);
// init write option
// this.syncWriteOption.sync(true);
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new RuntimeException(e);
}
logger.info("Level db created, db =" + dbFile);
}
@Override
public synchronized void put(ByteString key, ByteString oldVersion,
KVValue value, PersistOption pOption) throws KVStoreException {
ByteString version = null;
byte[] keyArray = key.toByteArray();
byte[] data = null;
data = db.get(keyArray);
KVValue obj = null;
if (data != null) {
obj = new KVValue(data);
version = obj.getVersion();
}
SimulatorEngine.logBytes("put, key", KvkOf(key).getKey());
checkVersion(version, oldVersion);
value.setKeyOf(key);
// write options
WriteOptions writeOptions = getWriteOption(pOption);
// put with write options
db.put(keyArray, value.toByteArray(), writeOptions);
}
@Override
public synchronized void putForced(ByteString key, KVValue value,
PersistOption pOption) throws KVStoreException {
byte[] keyArray = key.toByteArray();
value.setKeyOf(key);
// write options
WriteOptions writeOptions = getWriteOption(pOption);
// logger.info ("****** put writing option: " + wOptions.sync());
db.put(keyArray, value.toByteArray(), writeOptions);
}
@Override
public synchronized void delete(ByteString key, ByteString oldVersion,
PersistOption option) throws KVStoreException {
ByteString prevVersion = getVersion(key);
checkVersion(prevVersion, oldVersion);
// write options
WriteOptions writeOptions = getWriteOption(option);
// delete with write options
db.delete(key.toByteArray(), writeOptions);
}
@Override
public synchronized void deleteForced(ByteString key, PersistOption option)
throws KVStoreException {
// forced delete
// write options
WriteOptions writeOptions = getWriteOption(option);
// delete with write option
db.delete(key.toByteArray(), writeOptions);
}
@Override
public synchronized KVValue get(ByteString key) throws KVStoreException {
byte[] keyArray = key.toByteArray();
byte[] data = db.get(keyArray);
if (data == null) {
throw new KVStoreNotFound();
}
return new KVValue(data);
}
@Override
public synchronized KVValue getPrevious(ByteString key)
throws KVStoreException {
// get iterator
DBIterator dbit = db.iterator();
// get byte[]
byte[] kbytes = key.toByteArray();
KVValue value = null;
try {
// move to closest key
dbit.seek(kbytes);
// if there is a key smaller
if (dbit.hasPrev()) {
// get entry
Map.Entry<byte[], byte[]> entry = dbit.prev();
// return value
value = new KVValue(entry.getValue());
} else {
// go to the last key
dbit.seekToLast();
// check if there is an entry
if (dbit.hasNext()) {
// get entry
Map.Entry<byte[], byte[]> entry = dbit.next();
// logger.info("key=" + new String(entry.getKey()));
// compare last
int cv = compare(entry.getKey(), kbytes);
if (cv < 0) {
// return value
value = new KVValue(entry.getValue());
} else {
throw new KVStoreNotFound();
}
}
}
} catch (Exception e) {
//
throw new KVStoreNotFound();
} finally {
try {
dbit.close();
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
if (value == null) {
throw new KVStoreNotFound();
}
return value;
}
@Override
public synchronized KVValue getNext(ByteString key) throws KVStoreException {
DBIterator dbit = db.iterator();
KVValue value = null;
byte[] kbytes = key.toByteArray();
try {
dbit.seek(kbytes);
if (dbit.hasNext()) {
// get next element
Map.Entry<byte[], byte[]> entry = dbit.next();
if (compare(entry.getKey(), kbytes) == 0) {
// get next
entry = dbit.next();
}
value = new KVValue(entry.getValue());
} else {
throw new KVStoreNotFound();
}
} catch (NoSuchElementException ne) {
throw new KVStoreNotFound();
} finally {
try {
dbit.close();
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
return value;
}
@Override
public synchronized SortedMap<?, ?> getRange(ByteString startKey,
boolean startKeyInclusive, ByteString endKey,
boolean endKeyInclusive, int max) throws KVStoreException {
SortedMap<KVKey, KVValue> map = new TreeMap<KVKey, KVValue>();
byte[] start = startKey.toByteArray();
byte[] end = endKey.toByteArray();
// Short-circuit when the start key comes after the end key.
if (compare(start, end) > 0) {
return map;
}
if ((compare(start, end) == 0)
&& ((startKeyInclusive && endKeyInclusive) == false)) {
return map;
}
DBIterator cursor = null;
try {
cursor = db.iterator();
cursor.seek(start);
if (cursor.hasNext()) {
Entry<byte[], byte[]> e = cursor.next();
int cv = compare(e.getKey(), start);
if (cv == 0) {
if (startKeyInclusive) {
map.put(new KVKey(e.getKey()),
new KVValue(e.getValue()));
}
} else {
// check if should include
if (shouldInclude(e.getKey(), end, endKeyInclusive, false)) {
map.put(new KVKey(e.getKey()),
new KVValue(e.getValue()));
}
}
}
while (cursor.hasNext() && map.size() < max) {
Entry<byte[], byte[]> pair = cursor.next();
// check should we put the pair in the map
if (shouldInclude(pair.getKey(), end, endKeyInclusive, false)) {
map.put(new KVKey(pair.getKey()),
new KVValue(pair.getValue()));
}
}
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
// could get NoSuchElementException from getNext
throw new KVStoreException(e.getMessage());
} finally {
try {
cursor.close();
} catch (Exception ex2) {
logger.log(Level.WARNING, ex2.getMessage(), ex2);
}
}
return map;
}
@Override
public synchronized List<?> getRangeReversed(ByteString startKey,
boolean startKeyInclusive, ByteString endKey,
boolean endKeyInclusive, int max) throws KVStoreException {
List<KVKey> listOfKVKey = new ArrayList<KVKey>();
byte[] start = startKey.toByteArray();
byte[] end = endKey.toByteArray();
// Short-circuit when the start key comes after the end key.
if (compare(start, end) > 0) {
return listOfKVKey;
}
if ((compare(start, end) == 0)
&& ((startKeyInclusive && endKeyInclusive) == false)) {
return listOfKVKey;
}
DBIterator cursor = null;
try {
cursor = db.iterator();
cursor.seek(end);
boolean endKeyExist = cursor.hasNext();
Entry<byte[], byte[]> e = null;
// if end key does not exist, seek to last and add the last key if
// should include
if (!endKeyExist) {
cursor.seekToLast();
if (cursor.hasNext()) {
e = cursor.next();
if (shouldInclude(e.getKey(), start, startKeyInclusive,
true)) {
listOfKVKey.add(new KVKey(e.getKey()));
}
}
} else // if end key exists, add end key if should include
{
if (endKeyInclusive) {
e = cursor.next();
int cv = compare(e.getKey(), end);
if (cv == 0) {
listOfKVKey.add(new KVKey(e.getKey()));
}
if (0 > cv) {
// check if should include
if (shouldInclude(e.getKey(), start, startKeyInclusive,
true)) {
listOfKVKey.add(new KVKey(e.getKey()));
}
}
}
}
// if endKey exists, seek to endKey, or seek to last key
if (endKeyExist) {
cursor.seek(end);
} else {
cursor.seekToLast();
}
// move cursor to previous and add rest keys
while (cursor.hasPrev() && listOfKVKey.size() < max) {
Entry<byte[], byte[]> pair = cursor.prev();
// check should we put the pair in the map
if (shouldInclude(pair.getKey(), start, startKeyInclusive, true)) {
listOfKVKey.add(new KVKey(pair.getKey()));
}
}
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
// could get NoSuchElementException from getNext
throw new KVStoreException(e.getMessage());
} finally {
try {
cursor.close();
} catch (Exception ex2) {
logger.log(Level.WARNING, ex2.getMessage(), ex2);
}
}
return listOfKVKey;
}
@Override
public synchronized void close() {
try {
this.db.close();
logger.info("leveldb closed ...");
} catch (IOException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
@Override
public void reset() throws KVStoreException {
this.close();
// delete db file
File ldb = new File(this.dbFile);
// boolean deleted = deleteDirectory(ldb);
Options options = new Options();
try {
factory.destroy(ldb, options);
} catch (IOException e) {
throw new KVStoreException(e.getMessage());
}
logger.info("leveldb removed, path=" + this.dbFile);
// re open
this.init(config);
}
/**
* delete the specified directory.
*
* @param directory
* to be deleted
*
* @return true if directory is deleted. Otherwise, delete false.
*/
public static boolean deleteDirectory(File directory) {
// cannot be null
if (directory == null) {
throw new NullPointerException("file cannot be null");
}
// check if the directory exists
if (!directory.exists() || !directory.isDirectory()) {
return false;
}
// get list of files in the dir, cannot be null since it is a dir.
String[] fileNames = directory.list();
for (String fineName : fileNames) {
// get the file handle
File file = new File(directory, fineName);
if (file.isDirectory()) {
// recursive delete dir
deleteDirectory(file);
} else {
// delete file
file.delete();
}
}
// delete specified dir
return directory.delete();
}
private KVKey KvkOf(ByteString k) {
return new KVKey(k);
}
private void checkVersion(ByteString version, ByteString oldVersion)
throws KVStoreVersionMismatch {
logger.finest("Compare len, version size= " + mySize(version)
+ ", ols version size=" + mySize(oldVersion));
if (mySize(version) != mySize(oldVersion)) {
throw new KVStoreVersionMismatch("Length mismatch");
}
if (mySize(version) == 0)
return;
if (!version.equals(oldVersion)) {
throw new KVStoreVersionMismatch("Compare mismatch");
}
}
private int mySize(ByteString s) {
if (s == null)
return 0;
return s.size();
}
// returns the version if it is in the db. Null otherwise.
ByteString getVersion(ByteString key) throws KVStoreException {
KVValue obj = get(key);
if (obj == null)
throw new KVStoreNotFound();
if (!obj.hasVersion())
return ByteString.EMPTY;
return obj.getVersion();
}
private static boolean shouldInclude(byte[] k1, byte[] compareKey,
boolean inclusive, boolean reverse) {
int cv = compare(k1, compareKey);
if (cv == 0) {
if (inclusive) {
return true;
}
}
return reverse ? (cv > 0) : (cv < 0);
}
/**
*
* @param left
* @param right
* @return
*/
public static int compare(byte[] left, byte[] right) {
int l1 = left.length;
int l2 = right.length;
int len = Math.min(l1, l2);
for (int i = 0; i < len; i++) {
int a = (left[i] & 0xff);
int b = (right[i] & 0xff);
if (a != b) {
return a - b;
}
}
return left.length - right.length;
}
private static WriteOptions getWriteOption(PersistOption pOption) {
// write option
WriteOptions wOptions = null;
switch (pOption) {
case SYNC:
case FLUSH:
wOptions = SYNC_WRITE_OPTION;
break;
case ASYNC:
wOptions = asyncWriteOption;
break;
default:
wOptions = SYNC_WRITE_OPTION;
}
return wOptions;
}
@Override
public BatchOperation<ByteString, KVValue> createBatchOperation()
throws KVStoreException {
return new LdbBatchOperation(db);
}
@Override
public void flush() throws KVStoreException {
try {
doFlush();
} catch (Exception e) {
KVStoreException kvse = new KVStoreException(e.getMessage());
throw kvse;
}
}
public synchronized void doFlush() throws IOException {
// make a key so that no key in DB matches it
byte[] key = new byte[4 * 1024 + 1];
// fill with 0
Arrays.fill(key, (byte) 0);
// get value
byte[] data = db.get(key);
WriteBatch batch = db.createWriteBatch();
try {
if (data == null) {
/**
* no entry for key. perform no op
*/
batch.put(key, key);
batch.delete(key);
} else {
/**
* entry found, put back after delete
*/
batch.delete(key);
batch.put(key, data);
}
/**
* do batch operation with sync option.
*/
db.write(batch, SYNC_WRITE_OPTION);
logger.info("data flushed to db ....");
} finally {
// close the batch
batch.close();
}
}
@Override
public void compactRange(ByteString startKey, ByteString endKey)
throws KVStoreException {
try {
// start key
byte[] begin = null;
// end key
byte[] end = null;
if (startKey != null && startKey.isEmpty() == false) {
begin = startKey.toByteArray();
}
if (endKey != null && endKey.isEmpty() == false) {
end = endKey.toByteArray();
}
this.db.compactRange(begin, end);
logger.info("Media optimization finished");
} catch (Exception e) {
throw new KVStoreException(e.getMessage());
}
}
@Override
public String getPersistStorePath() throws KVStoreException {
return this.persistFolder;
}
}