/*
* JBoss, Home of Professional Open Source
* Copyright 2009 Red Hat Inc. and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.loaders.jdbm;
import jdbm.RecordManager;
import jdbm.RecordManagerFactory;
import jdbm.btree.BTree;
import jdbm.helper.FastIterator;
import jdbm.helper.Tuple;
import jdbm.helper.TupleBrowser;
import jdbm.htree.HTree;
import net.jcip.annotations.ThreadSafe;
import org.infinispan.Cache;
import org.infinispan.CacheException;
import org.infinispan.config.ConfigurationException;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.container.entries.InternalCacheValue;
import org.infinispan.loaders.AbstractCacheStore;
import org.infinispan.loaders.CacheLoaderConfig;
import org.infinispan.loaders.CacheLoaderException;
import org.infinispan.loaders.CacheLoaderMetadata;
import org.infinispan.loaders.modifications.Modification;
import org.infinispan.loaders.modifications.Remove;
import org.infinispan.loaders.modifications.Store;
import org.infinispan.marshall.StreamingMarshaller;
import org.infinispan.loaders.jdbm.logging.Log;
import org.infinispan.util.SysPropertyActions;
import org.infinispan.util.logging.LogFactory;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.text.SimpleDateFormat;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* A persistent <code>CacheLoader</code> based on the JDBM project. See http://jdbm.sourceforge.net/ . Does not support
* transaction isolation.
* <p/>
* Supports removal of expired entries.
* <p/>
* It would probably be better if meta-data (expiry time) was stored independent of the value of the entry. That is, if
* (key,"m") == meta and (key,"v") == value.
*
* @author Elias Ross
* @author Galder ZamarreƱo
*/
@ThreadSafe
@CacheLoaderMetadata(configurationClass = JdbmCacheStoreConfig.class)
public class JdbmCacheStore extends AbstractCacheStore {
private static final Log log = LogFactory.getLog(JdbmCacheStore.class, Log.class);
private static final boolean trace = log.isTraceEnabled();
private static final String NAME = "CacheLoader";
private static final String EXPIRY = "Expiry";
private static final String DATE = "HH:mm:ss.SSS";
private BlockingQueue<ExpiryEntry> expiryEntryQueue;
private JdbmCacheStoreConfig config;
private RecordManager recman;
private HTree tree;
private BTree expiryTree;
@Override
public Class<? extends CacheLoaderConfig> getConfigurationClass() {
return JdbmCacheStoreConfig.class;
}
@Override
public void init(CacheLoaderConfig clc, Cache<?, ?> cache, StreamingMarshaller m) throws CacheLoaderException {
super.init(clc, cache, m);
this.config = (JdbmCacheStoreConfig) clc;
}
@Override
public void start() throws CacheLoaderException {
String locationStr = config.getLocation();
if (locationStr == null) {
locationStr = SysPropertyActions.getProperty("java.io.tmpdir");
config.setLocation(locationStr);
}
expiryEntryQueue = new LinkedBlockingQueue<ExpiryEntry>(config.getExpiryQueueSize());
// JBCACHE-1448 db name parsing fix courtesy of Ciro Cavani
/* Parse config string. */
int offset = locationStr.indexOf('#');
String cacheDbName;
if (offset >= 0 && offset < locationStr.length() - 1) {
cacheDbName = locationStr.substring(offset + 1);
locationStr = locationStr.substring(0, offset);
} else {
cacheDbName = cache.getName();
if (cacheDbName == null)
cacheDbName = "jdbm";
}
// test location
File location = new File(locationStr);
if (!location.exists()) {
boolean created = location.mkdirs();
if (!created)
throw new ConfigurationException("Unable to create cache loader location " + location);
}
if (!location.isDirectory()) {
throw new ConfigurationException("Cache loader location [" + location + "] is not a directory!");
}
try {
openDatabase(new File(location, cacheDbName));
} catch (Exception e) {
throw new ConfigurationException(e);
}
log.debug("cleaning up expired entries...");
purgeInternal();
log.debug("started");
super.start();
}
@Override
public InternalCacheEntry load(Object key) throws CacheLoaderException {
try {
InternalCacheEntry ice = unmarshall(tree.get(key), key);
if (ice != null && ice.isExpired(System.currentTimeMillis())) {
remove(key);
return null;
}
return ice;
} catch (IOException e) {
throw new CacheLoaderException(e);
} catch (ClassNotFoundException e) {
throw new CacheException(e);
}
}
@Override
public Set<InternalCacheEntry> loadAll() throws CacheLoaderException {
return new BTreeSet();
}
@Override
public Set<InternalCacheEntry> load(int numEntries) throws CacheLoaderException {
return new BTreeSet(numEntries);
}
@Override
public Set<Object> loadAllKeys(Set<Object> keysToExclude) throws CacheLoaderException {
try {
Set<Object> s = new HashSet<Object>();
FastIterator fi = tree.keys();
Object o;
while ((o = fi.next()) != null) if (keysToExclude == null || !keysToExclude.contains(o)) s.add(o);
return s;
} catch (IOException e) {
throw new CacheLoaderException(e);
}
}
/**
* Opens all databases and initializes database related information.
*/
private void openDatabase(File f) throws Exception {
Properties props = new Properties();
// Incorporate properties from setConfig() ?
// props.put(RecordManagerOptions.SERIALIZER,
// RecordManagerOptions.SERIALIZER_EXTENSIBLE);
// props.put(RecordManagerOptions.PROFILE_SERIALIZATION, "false");
recman = RecordManagerFactory.createRecordManager(f.toString(), props);
long recid = recman.getNamedObject(NAME);
log.debugf("%s located as %d", NAME, recid);
if (recid == 0) {
createTree();
} else {
tree = HTree.load(recman, recid);
recid = recman.getNamedObject(EXPIRY);
expiryTree = BTree.load(recman, recid);
setSerializer();
}
log.jdbmDbOpened(f);
}
/**
* Resets the value serializer to point to our marshaller.
*/
private void setSerializer() {
// TODO explore how to use our marshaller with HTree
// tree.setValueSerializer(new JdbmSerializer(getMarshaller()));
expiryTree.setValueSerializer(new JdbmSerializer(getMarshaller()));
}
private void createTree() throws IOException {
tree = HTree.createInstance(recman);
expiryTree = BTree.createInstance(recman, new NaturalComparator(), null, null);
recman.setNamedObject(NAME, tree.getRecid());
recman.setNamedObject(EXPIRY, expiryTree.getRecid());
setSerializer();
}
/**
* Closes all databases, ignoring exceptions, and nulls references to all database related information.
*/
@Override
public void stop() throws CacheLoaderException {
super.stop();
if (recman != null) {
try {
recman.close();
} catch (IOException e) {
throw new CacheException(e);
}
}
recman = null;
tree = null;
expiryTree = null;
}
@Override
public void clear() throws CacheLoaderException {
if (trace)
log.trace("clear()");
try {
recman.delete(tree.getRecid());
recman.delete(expiryTree.getRecid());
createTree();
} catch (IOException e) {
throw new CacheLoaderException(e);
}
}
@Override
public boolean remove(Object key) throws CacheLoaderException {
try {
return remove0(key);
} finally {
commit();
}
}
private void commit() throws CacheLoaderException {
try {
recman.commit();
} catch (IOException e) {
throw new CacheLoaderException(e);
}
}
public boolean remove0(Object key) throws CacheLoaderException {
if (trace)
log.tracef("remove() %s", key);
try {
// Not the most efficient way but JDBM offers no other API
boolean ret = tree.get(key) != null;
tree.remove(key);
// If the key does not exist, HTree ignores the operation, so always return true
return ret;
} catch (IOException e) {
// can happen during normal operation
return false;
}
}
@Override
public void store(InternalCacheEntry entry) throws CacheLoaderException {
store0(entry);
commit();
}
private byte[] marshall(InternalCacheEntry entry) throws IOException, InterruptedException {
return getMarshaller().objectToByteBuffer(entry.toInternalCacheValue());
}
private InternalCacheEntry unmarshall(Object o, Object key) throws IOException, ClassNotFoundException {
if (o == null)
return null;
byte b[] = (byte[]) o;
InternalCacheValue v = (InternalCacheValue) getMarshaller().objectFromByteBuffer(b);
return v.toInternalCacheEntry(key);
}
private void store0(InternalCacheEntry entry) throws CacheLoaderException {
Object key = entry.getKey();
if (trace)
log.tracef("store() %s", key);
try {
tree.put(key, marshall(entry));
if (entry.canExpire())
addNewExpiry(entry);
} catch (IOException e) {
throw new CacheLoaderException(e);
} catch (InterruptedException ie) {
if (trace) log.trace("Interrupted while marshalling entry");
Thread.currentThread().interrupt();
}
}
private void addNewExpiry(InternalCacheEntry entry) throws IOException {
long expiry = entry.getExpiryTime();
if (entry.getMaxIdle() > 0) {
// Coding getExpiryTime() for transient entries has the risk of being a moving target
// which could lead to unexpected results, hence, InternalCacheEntry calls are required
expiry = entry.getMaxIdle() + System.currentTimeMillis();
}
Long at = expiry;
Object key = entry.getKey();
if (trace) log.tracef("at %s expire %s", new SimpleDateFormat(DATE).format(new Date(at)), key);
try {
expiryEntryQueue.put(new ExpiryEntry(at, key));
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interruption status
}
}
/**
* Writes to a stream the number of entries (long) then the entries themselves.
*/
@Override
public void toStream(ObjectOutput out) throws CacheLoaderException {
try {
Set<InternalCacheEntry> loadAll = loadAll();
log.debug("toStream() entries");
int count = 0;
for (InternalCacheEntry entry : loadAll) {
getMarshaller().objectToObjectStream(entry, out);
count++;
}
getMarshaller().objectToObjectStream(null, out);
log.debugf("wrote %d entries", count);
} catch (IOException e) {
throw new CacheLoaderException(e);
}
}
/**
* Reads from a stream the number of entries (long) then the entries themselves.
*/
@Override
public void fromStream(ObjectInput in) throws CacheLoaderException {
try {
log.debug("fromStream()");
int count = 0;
while (true) {
count++;
InternalCacheEntry entry = (InternalCacheEntry) getMarshaller().objectFromObjectStream(in);
if (entry == null)
break;
store(entry);
}
log.debugf("read %d entries", count);
} catch (IOException e) {
throw new CacheLoaderException(e);
} catch (ClassNotFoundException e) {
throw new CacheLoaderException(e);
} catch (InterruptedException ie) {
if (log.isTraceEnabled()) log.trace("Interrupted while reading from stream");
Thread.currentThread().interrupt();
}
}
/**
* Purge expired entries.
*/
@Override
protected void purgeInternal() throws CacheLoaderException {
log.trace("purgeInternal");
try {
purgeInternal0();
} catch (Exception e) {
throw new CacheLoaderException(e);
}
}
/**
* Find all times less than current time. Build a list of keys for those times. Then purge those keys, assuming those
* keys' expiry has not changed.
*
* @throws ClassNotFoundException
*/
private void purgeInternal0() throws Exception {
// Drain queue and update expiry tree
List<ExpiryEntry> entries = new ArrayList<ExpiryEntry>();
expiryEntryQueue.drainTo(entries);
for (ExpiryEntry entry : entries) {
Object existing = expiryTree.insert(entry.expiry, entry.key, false);
if (existing != null) {
// in the case of collision make the key a List ...
if (existing instanceof List) {
((List<Object>) existing).add(entry.key);
expiryTree.insert(entry.expiry, existing, true);
} else {
List<Object> al = new ArrayList<Object>(2);
al.add(existing);
al.add(entry.key);
expiryTree.insert(entry.expiry, al, true);
}
}
}
// Browse the expiry and remove accordingly
TupleBrowser browse = expiryTree.browse();
Tuple tuple = new Tuple();
List<Long> times = new ArrayList<Long>();
List<Object> keys = new ArrayList<Object>();
while (browse.getNext(tuple)) {
Long time = (Long) tuple.getKey();
if (time > System.currentTimeMillis())
break;
times.add(time);
Object key = tuple.getValue();
if (key instanceof List)
keys.addAll((List<?>) key);
else
keys.add(key);
}
for (Long time : times) {
expiryTree.remove(time);
}
if (!keys.isEmpty())
log.debugf("purge (up to) %d entries", keys.size());
int count = 0;
long currentTimeMillis = System.currentTimeMillis();
for (Object key : keys) {
byte[] b = (byte[]) tree.get(key);
if (b == null)
continue;
InternalCacheValue ice = (InternalCacheValue) getMarshaller().objectFromByteBuffer(b);
if (ice.isExpired(currentTimeMillis)) {
// somewhat inefficient to FIND then REMOVE...
tree.remove(key);
count++;
}
}
if (count != 0)
log.debugf("purged %d entries", count);
recman.commit();
}
@Override
protected void applyModifications(List<? extends Modification> mods) throws CacheLoaderException {
for (Modification m : mods) {
switch (m.getType()) {
case STORE:
store0(((Store) m).getStoredEntry());
break;
case CLEAR:
clear();
break;
case REMOVE:
remove0(((Remove) m).getKey());
break;
default:
throw new AssertionError();
}
}
commit();
}
@Override
public String toString() {
BTree et = expiryTree;
int expiry = (et == null) ? -1 : et.size();
return "JdbmCacheLoader locationStr=" + config.getLocation() + " expirySize=" + expiry;
}
private final class BTreeSet extends AbstractSet<InternalCacheEntry> {
int maxSize = -1;
private BTreeSet(int maxSize) {
this.maxSize = maxSize;
}
private BTreeSet() {
}
@Override
public Iterator<InternalCacheEntry> iterator() {
final FastIterator fi;
try {
fi = tree.keys();
} catch (IOException e) {
throw new CacheException(e);
}
return new Iterator<InternalCacheEntry>() {
int entriesReturned = 0;
InternalCacheEntry current = null;
boolean next = true;
@Override
public boolean hasNext() {
if (current == null && next) {
Object key = fi.next();
if (key == null) {
next = false;
} else {
try {
current = unmarshall(tree.get(key), key);
} catch (IOException e) {
throw new CacheException(e);
} catch (ClassNotFoundException e) {
throw new CacheException(e);
}
}
}
if (next == true && entriesReturned >= maxSize && maxSize > -1) next = false;
return next;
}
@Override
public InternalCacheEntry next() {
if (!hasNext())
throw new NoSuchElementException();
try {
entriesReturned++;
return current;
} finally {
current = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
@SuppressWarnings("unused")
public int size() {
log.warn("size() should never be called; except for tests");
int size = 0;
for (Object dummy : this)
size++;
return size;
}
}
private static final class ExpiryEntry {
private final Long expiry;
private final Object key;
private ExpiryEntry(long expiry, Object key) {
this.expiry = expiry;
this.key = key;
}
}
}