package com.cloudhopper.datastore.tokyo;
/*
* #%L
* ch-datastore
* %%
* Copyright (C) 2012 Cloudhopper by Twitter
* %%
* Licensed 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.
* #L%
*/
import com.cloudhopper.commons.util.HexUtil;
import com.cloudhopper.commons.util.StringUtil;
import com.cloudhopper.datastore.*;
import java.io.File;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tokyocabinet.BDB;
/**
* Tokyo Cabinet backed implementation of a DataStore.
*
* @author joelauer
*/
public class TokyoDataStore extends BaseDataStore implements AscendingIteratorSupport {
private final static Logger logger = LoggerFactory.getLogger(TokyoDataStore.class);
// path used to generate a full path to the database file
private File path;
// the underlying b-tree database
private BDB bdb;
// for performance, the database is not synchronized on every put() and take()
// if -1, sync() is disabled
// if 0, sync() is called on ever put() and take()
// if > 0, the amount of ms between a sync() call is made, set to 5 seconds by default
private long synchronizeInterval;
private Timer synchronizeTimer;
private TimerTask synchronizeTimerTask;
public TokyoDataStore() {
super("TC-BDB");
// defaults
synchronizeInterval = 5000;
}
/**
* Returns the underlying B-Tree database. If not opened, this will return
* null.
* @return Null if not open, otherwise the B-Tree database.
*/
public BDB getDatabase() {
return bdb;
}
/**
* Gets the interval between sync() calls to underlying Tokyo cabinet db.
* For performance, by default, the database is not synchronized on every
* put() and take(). If -1, sync() is disabled. If 0, sync() is called on
* every put() and take(). Throughput will be dramatically decreased, but
* it'll guarantee the data makes it into the file. If > 0, its the length
* of ms between a sync() call is made, set to 5 seconds by default
* @return The synchronization interval in milliseconds
*/
public long getSynchronizeInterval() {
return synchronizeInterval;
}
/**
*
* @throws QueueStoreException Thrown if the data store is already open
* when this method is called.
*/
public void setSynchronizeInterval(long value) {
this.synchronizeInterval = value;
}
private File getDatabaseFile() {
return new File(getDirectory(), getName() + ".bdb");
}
@Override
public void doDelete() throws DataStoreFatalException {
File databaseFile = getDatabaseFile();
try {
databaseFile.delete();
} catch (Exception e) {
throw new DataStoreFatalException("Unable to delete " + databaseFile.getPath(), e);
}
}
@Override
public void doOpen() throws DataStoreFatalException {
// create a tokyo cabinet backed queue
bdb = new BDB();
// just leave comparison as LEXICAL, don't set it to anything else
/*
// comparator that does a byte at a time, up to the length of the shorter key
bdb.setcomparator(new tokyocabinet.BDBCMP() {
public int compare(byte[] a, byte[] b) {
int itlen = a.length;
if (itlen > b.length) itlen = b.length;
for (int i=0;i<itlen;i++) {
if (a[i] == b[i]) continue;
else return a[i] - b[i];
}
return 0;
}
});
*/
// try to open the database
path = getDatabaseFile();
if (!bdb.open(path.getAbsolutePath(), BDB.OWRITER | BDB.OCREAT)) {
// error occurred while opening, throw error
int ecode = bdb.ecode();
bdb = null;
throw new DataStoreFatalException("Unable to open " + toString() + " @ " + path + " {code=" + ecode + ", message=" + BDB.errmsg(ecode) + "}");
}
if (this.synchronizeInterval < 0) {
logger.debug("Automatic sync disabled {synchronizeInterval < 0} for " + toString());
} else if (this.synchronizeInterval == 0) {
logger.debug("Automatic sync enabled for every transaction {synchronizeInterval == 0} for " + toString());
} else {
// schedule task to print results every 5 seconds
synchronizeTimer = new Timer("TokyoDataStoreSync-" + getName(), true);
synchronizeTimerTask = new TimerTask() {
@Override
public void run() {
try {
if (bdb != null) {
logger.trace("Synchronized data store [name=" + getName() + "]");
bdb.sync();
} else {
// cancel this task!
logger.warn("Cancelling synchronizeTimerTask since bdb was null, this may be normal if data store shutting down");
this.cancel();
}
} catch (Exception e) {
logger.error("", e);
}
}
};
synchronizeTimer.schedule(synchronizeTimerTask, synchronizeInterval, synchronizeInterval);
logger.debug("Automatic sync enabled every " + synchronizeInterval + " ms for " + toString());
}
}
@Override
public void doClose() throws DataStoreFatalException {
try {
// make sure sync thread is cancelled
if (synchronizeTimerTask != null) {
try {
synchronizeTimerTask.cancel();
} catch (Exception e) {
// ignore this
}
synchronizeTimerTask = null;
}
if (synchronizeTimer != null) {
try {
synchronizeTimer.cancel();
} catch (Exception e) {
// ignore this
}
synchronizeTimer = null;
}
if (bdb != null) {
if (!bdb.close()) {
// error occurred while opening, throw error
int ecode = bdb.ecode();
throw new DataStoreFatalException("Unable to properly close DataStore {code=" + ecode + ", message=" + BDB.errmsg(ecode) + "}");
}
}
} finally {
bdb = null;
}
}
@Override
public void doSetRecord(byte[] key, byte[] value) throws DataStoreFatalException {
// bdb.put = returns true if value put in and didn't replace anything
// returns false if it failed OR if it had to replace a value
if (!bdb.put(key, value)) {
int ecode = bdb.ecode();
// this code may be 0 if the value was replaced
if (ecode == 0) {
// value replaced, ignoring
} else {
throw new DataStoreFatalException("Unable to set value and key [0x" + HexUtil.toHexString(key) + "] [code=" + ecode + ", message=" + BDB.errmsg(ecode) + "]");
}
}
// if synchronize interval is zero, sync() on every put() and take()
if (this.synchronizeInterval == 0) {
// syncing the b-tree database
bdb.sync();
}
}
@Override
public byte[] doGetRecord(byte[] key) throws RecordNotFoundException, DataStoreFatalException {
byte[] value = bdb.get(key);
if (value == null) {
throw new RecordNotFoundException("The record for key [0x" + HexUtil.toHexString(key) + "] was not found");
}
return value;
}
@Override
public void doDeleteRecord(byte[] key) throws RecordNotFoundException, DataStoreFatalException {
if (!bdb.out(key)) {
int ecode = bdb.ecode();
// this code may be 0 if the value was replaced
if (ecode == 0) {
// value deleted, ignoring error code
} else if (ecode == 22) {
//[code=22, message=no record found]
throw new RecordNotFoundException("The record for key [0x" + HexUtil.toHexString(key) + "] was not found");
} else {
throw new DataStoreFatalException("Unable to delete record with key [0x" + HexUtil.toHexString(key) + "] [code=" + ecode + ", message=" + BDB.errmsg(ecode) + "]");
}
}
// if synchronize interval is zero, sync() on every put() and take()
if (this.synchronizeInterval == 0) {
// syncing the b-tree database
bdb.sync();
}
}
public DataStoreIterator doGetAscendingIterator() throws DataStoreFatalException {
return new TokyoDataStoreIterator(getDatabase());
}
}