
com.tectonica.collections.KeyValueStore Maven / Gradle / Ivy
package com.tectonica.collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import com.tectonica.collections.KeyValueStore.KeyValue;
/**
* Simple, yet powerful, framework (and approach) for handling a key-value store. It allows for multiple concurrent readers but a single
* concurrent writer on each entry. It also provides a read-before-update mechanism which makes life simpler and keeps data consistent.
* Indexing is also supported as part of the framework. The interface is intuitive and straightforward. This class itself is abstract and
* subclasses are included for several backend persistence engines, including in-memory (which is great for development).
*
* The usage of this framework would probably yield the most benefit when used to prototype a data model, where changes are frequent and not
* backwards-compatible. However, in more than a few scenarios, this framework can be used in production. The heavy-lifting is done by the
* backend database and cache either way.
*
* @author Zach Melamed
*/
public abstract class KeyValueStore implements Iterable>
{
public interface KeyValue
{
K getKey();
V getValue();
}
public interface KeyMapper
{
public K getKeyOf(V value);
}
protected final KeyMapper keyMapper;
/**
* creates a new key-value store manager (this doesn't indicate creation of a brand new storage unit (e.g. table), only a creation of
* this class, which manages a newly-created/pre-existing storage)
*
* @param keyMapper
* this optional parameter is suitable in situations where the key of an entry can be inferred from its value directly
* (as opposed to when the key and value are stored separately). when provided, several convenience methods become applicable
*/
protected KeyValueStore(KeyMapper keyMapper)
{
this.keyMapper = keyMapper;
cache = createCache();
usingCache = (cache != null);
}
/***********************************************************************************
*
* CACHE
*
***********************************************************************************/
protected static interface Cache
{
V get(K key);
Map get(Collection keys);
void put(K key, V value);
void put(Map values);
void delete(K key);
void deleteAll();
}
/**
* overridden by subclasses that wish to support a caching mechanism
*/
protected Cache createCache()
{
return null;
}
public void clearCache()
{
if (usingCache)
cache.deleteAll();
}
protected final Cache cache;
protected final boolean usingCache;
/* *********************************************************************************
*
* GETTERS
*
* many of the non-abstract methods here offer somewhat of a naive implementation.
* subclasses are welcome to override with their own efficient implementation.
*
* *********************************************************************************
*/
protected abstract V dbRead(K key);
protected abstract Iterator> dbIterate(Collection keys);
@Override
public abstract Iterator> iterator(); // gets ALL entries, bypasses cache
// ///////////////////////////////////////////////////////////////////////////////////////
public V get(K key)
{
if (!usingCache)
return dbRead(key);
V value = cache.get(key);
if (value == null)
{
value = dbRead(key);
if (value != null)
cache.put(key, value);
}
return value;
}
public Iterator> iteratorFor(final Collection keys)
{
return iteratorFor(keys, false);
}
public Iterator> iteratorFor(final Collection keys, final boolean postponeCaching)
{
if (keys.isEmpty())
return Collections.emptyIterator();
if (!usingCache)
return dbIterate(keys);
final Map cachedValues = cache.get(keys);
final Iterator> dbIter;
if (cachedValues.size() == keys.size())
dbIter = Collections.emptyIterator(); // all keys were found in cache
else
{
final Collection uncachedKeys;
if (cachedValues.size() == 0) // no key was found on cache
uncachedKeys = keys;
else
{
uncachedKeys = new ArrayList<>();
for (K key : keys)
if (!cachedValues.containsKey(key))
uncachedKeys.add(key);
}
if (uncachedKeys.isEmpty())
dbIter = Collections.emptyIterator(); // possible only when duplicate keys were passed as input
else
dbIter = dbIterate(uncachedKeys);
}
return new Iterator>()
{
private Iterator keysIter = keys.iterator();
private KeyValue dbNext = dbIter.hasNext() ? dbIter.next() : null;
private KeyValue nextItem = null;
private Map toCache = new HashMap<>();
@Override
public boolean hasNext()
{
if (nextItem != null)
return true;
while (keysIter.hasNext())
{
K key = keysIter.next();
// try from db
if (dbNext != null && dbNext.getKey().equals(key))
{
// cache it first (or mark for postponed caching)
V value = dbNext.getValue();
if (!postponeCaching)
cache.put(key, value);
else
toCache.put(key, value);
// take value and move db-pointer to next entry
nextItem = dbNext;
dbNext = dbIter.hasNext() ? dbIter.next() : null;
return true;
}
// try from cache
V value = cachedValues.get(key);
if (value != null)
{
nextItem = keyValueOf(key, value);
return true;
}
}
if (dbIter.hasNext())
throw new RuntimeException("Internal error in cache-based iteration");
if (postponeCaching && toCache != null)
{
if (!toCache.isEmpty())
cache.put(toCache);
toCache = null;
}
return false; // i.e. nextVal is null
}
@Override
public KeyValue next()
{
if (!hasNext())
throw new NoSuchElementException();
KeyValue next = nextItem;
nextItem = null;
return next;
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Iterator keyIterator()
{
final Iterator> iter = iterator();
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public K next()
{
return iter.next().getKey();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Iterator valueIterator()
{
final Iterator> iter = iterator();
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public V next()
{
return iter.next().getValue();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Iterator valueIteratorFor(Collection keys)
{
return valueIteratorFor(keys, false);
}
public Iterator valueIteratorFor(Collection keys, boolean postponeCaching)
{
if (keys.isEmpty())
return Collections.emptyIterator();
final Iterator> iter = iteratorFor(keys, postponeCaching);
return new Iterator()
{
@Override
public boolean hasNext()
{
return iter.hasNext();
}
@Override
public V next()
{
return iter.next().getValue();
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
public Set keySet()
{
return iterateInto(keyIterator(), new HashSet());
}
public List values()
{
return iterateInto(valueIterator(), new ArrayList());
}
public List valuesFor(Collection keys)
{
if (keys.isEmpty())
return Collections.emptyList();
return iterateInto(valueIteratorFor(keys, true), new ArrayList());
}
/* *********************************************************************************
*
* SETTERS (PROTOCOL)
*
* *********************************************************************************
*/
/**
* an interface for managing a modification process of an existing entry. there are two types of such modification:
*
* - using {@link KeyValueStore#replace(Object, Object)}: in such case only the {@link #dbWrite(Object)} method will be invoked. it
* will be passed an updated value for an existing key.
*
- using {@link KeyValueStore#update(Object, Updater)}: in such case first the {@link #getModifiableValue()} method will be invoked,
* generating an instance for the caller to safely modify, and then the {@link #dbWrite(Object)} method will be invoked on that modified
* instance.
*
* both methods are invoked under the concurrency protection a lock provided with {@link KeyValueStore#getModificationLock(Object)}.
*/
protected interface Modifier
{
/**
* Returns an instance that can be safely modified by the caller. During this modification, calls to {@link #getValue()} will return
* the unchanged value. If the instance was indeed modified by the caller, and no exception occurred in the process, the method
* {@link #dbWrite(Object)} will be invoked.
*
* NOTE: this method is called only on a locked entry
*/
V getModifiableValue();
/**
* Makes the changes to an entry permanent. After this method finishes, calls to {@link #getValue()} will return the updated value.
*
* NOTE: this method is called only on a locked entry
*/
void dbWrite(V value);
}
protected static enum ModificationType
{
UPDATE, REPLACE;
}
/**
* required to return a (short-lived) instance of {@link Modifier} corresponding to a given key, or a null if the passed key can't be
* updated (because it doesn't exist, or for another reason). The returned instance is used for a one-time modification.
*/
protected abstract Modifier getModifier(K key, ModificationType purpose);
/**
* expected to return a global lock for a specific key (global means that it blocks other machines as well, not just the current
* instance). It is a feature of this framework that only one updater is allowed for an entry at each given moment, so whenever an
* updater thread starts the (non-atomic) process of updating the entry, all other attempts should be blocked.
*/
protected abstract Lock getModificationLock(K key);
public static abstract class Updater
{
private boolean stopped = false;
private boolean changed = false;
protected void stopIteration()
{
stopped = true;
}
public boolean isChanged()
{
return changed;
}
/**
* method inside which an entry may be safely modified with no concurrency or shared-state concerns
*
* @param value
* thread-safe entry on which the modifications are to be performed. never a null.
* @return
* true if the method indeed changed the entry
*/
public abstract boolean update(V value);
/**
* executes after the persistence has happened and getters return the new value. however, the entry is still locked at that point
* and can be addressed without any concern that another updater tries to start a modification.
*
* IMPORTANT: do not make any modifications to the passed entry inside this method
*/
public void postCommit(V value)
{}
/**
* called if an update process was requested on a non existing key
*/
public void entryNotFound()
{}
}
/* *********************************************************************************
*
* SETTERS
*
* *********************************************************************************
*/
protected abstract void dbInsert(K key, V value);
/**
* inserts a new entry, whose key doesn't already exist in storage. it's a faster and more resource-efficient way to insert entries
* compared to {@link #replace(Object, Object)} as it doesn't use any locking. do not use if you're not completely sure whether the key
* already exists. the behaviour of the store in such case is undetermined and implementation-dependent.
*/
public void insert(K key, V value)
{
fireEvent(EventType.PreInsert, key, value);
dbInsert(key, value);
if (usingCache)
cache.put(key, value);
}
/**
* inserts or updates an entry. if you're sure that the entry is new (i.e. its key doesn't already exist), use the more efficient
* {@link #insert(Object, Object)} instead
*/
public void replace(K key, V value)
{
Lock lock = getModificationLock(key);
lock.lock();
try
{
Modifier modifier = getModifier(key, ModificationType.REPLACE);
if (modifier == null)
insert(key, value);
else
{
fireEvent(EventType.PreReplace, key, value);
modifier.dbWrite(value);
if (usingCache)
cache.put(key, value);
}
}
finally
{
lock.unlock();
}
}
public V update(K key, Updater updater)
{
Lock lock = getModificationLock(key);
lock.lock();
try
{
Modifier modifier = getModifier(key, ModificationType.UPDATE);
if (modifier == null)
{
updater.entryNotFound();
return null;
}
V value = modifier.getModifiableValue();
if (value == null)
{
updater.entryNotFound();
return null;
}
fireEvent(EventType.PreUpdate, key, value);
updater.changed = updater.update(value);
if (updater.changed)
{
fireEvent(EventType.PreCommit, key, value);
modifier.dbWrite(value);
if (usingCache)
cache.put(key, value);
}
updater.postCommit(value);
return value;
}
finally
{
lock.unlock();
}
}
public void update(Collection keys, Updater updater)
{
for (K key : keys)
{
update(key, updater);
if (updater.stopped)
break;
}
}
/* *********************************************************************************
*
* SETTERS (CONVENIENCE)
*
* *********************************************************************************
*/
/**
* convenience method to update all entries
*/
public void updateAll(Updater updater)
{
update(keySet(), updater);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #update(Object, Updater)}
*/
public V updateValue(V value, Updater updater)
{
return update(keyMapper.getKeyOf(value), updater);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #insert(Object, Object)}
*/
public void insertValue(V value)
{
insert(keyMapper.getKeyOf(value), value);
}
/**
* convenience method applicable when {@code keyMapper} is provided
*
* @see {@link #replace(Object, Object)}
*/
public void replaceValue(V value)
{
replace(keyMapper.getKeyOf(value), value);
}
/* *********************************************************************************
*
* DELETERS
*
* *********************************************************************************
*/
protected abstract boolean dbDelete(K key);
protected abstract int dbDeleteAll();
public boolean delete(K key)
{
if (usingCache)
cache.delete(key);
return dbDelete(key);
}
public int deleteAll()
{
if (usingCache)
cache.deleteAll();
return dbDeleteAll();
}
/* *********************************************************************************
*
* EVENTS
*
* *********************************************************************************
*/
protected ConcurrentMultimap> handlers = new ConcurrentMultimap<>();
public static enum EventType
{
PreUpdate, PreCommit, PreInsert, PreReplace;
}
public interface EventHandler
{
public void handle(EventType type, K key, V value);
}
public void addListener(EventType type, EventHandler handler)
{
handlers.put(type, handler);
}
protected void fireEvent(EventType type, K key, V value)
{
Set> events = handlers.get(type);
if (events != null)
for (EventHandler event : events)
event.handle(type, key, value);
}
/* *********************************************************************************
*
* INDEXES
*
* *********************************************************************************
*/
public abstract Index createIndex(String indexName, IndexMapper mapFunc);
public interface IndexMapper
{
public F getIndexedFieldOf(V value);
}
public static abstract class Index
{
protected final IndexMapper mapper;
protected final String name;
protected Index(IndexMapper mapper, String name)
{
this.mapper = mapper;
this.name = name;
}
// ///////////////////////////////////////////////////////////////////////////////////////
public abstract Iterator> iteratorOf(F f);
public abstract Iterator keyIteratorOf(F f);
public abstract Iterator valueIteratorOf(F f);
// ///////////////////////////////////////////////////////////////////////////////////////
public boolean keyExistsOf(F f)
{
Iterator iter = keyIteratorOf(f);
return (iter.hasNext());
}
public Set keySetOf(F f)
{
return iterateInto(keyIteratorOf(f), new HashSet());
}
public List valuesOf(F f)
{
return iterateInto(valueIteratorOf(f), new ArrayList());
}
public List> entriesOf(F f)
{
return iterateInto(iteratorOf(f), new ArrayList>());
}
public Iterable> asIterableOf(F f)
{
return iterableOf(iteratorOf(f));
}
public Iterable asKeyIterableOf(F f)
{
return iterableOf(keyIteratorOf(f));
}
public Iterable asValueIterableOf(F f)
{
return iterableOf(valueIteratorOf(f));
}
public KeyValue getFirstEntry(F f)
{
Iterator> iter = iteratorOf(f);
if (iter.hasNext())
return iter.next();
return null;
}
public K getFirstKey(F f)
{
Iterator iter = keyIteratorOf(f);
if (iter.hasNext())
return iter.next();
return null;
}
public V getFirstValue(F f)
{
Iterator iter = valueIteratorOf(f);
if (iter.hasNext())
return iter.next();
return null;
}
public String getName()
{
return name;
}
}
/* *********************************************************************************
*
* INTERNAL UTILS
*
* *********************************************************************************
*/
protected KeyValue keyValueOf(final K key, final V value)
{
return new KeyValue()
{
@Override
public K getKey()
{
return key;
}
@Override
public V getValue()
{
return value;
}
};
}
protected static > T iterateInto(Iterator iter, T collection)
{
while (iter.hasNext())
collection.add(iter.next());
return collection;
}
protected static Iterable iterableOf(final Iterator iter)
{
return new Iterable()
{
@Override
public Iterator iterator()
{
return iter;
}
};
}
}