
com.pippsford.util.DeferredWriteMap Maven / Gradle / Ivy
Show all versions of common-utils Show documentation
package com.pippsford.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* A Map which can be read concurrently and allows for updates. The map that is being read from is rarely updated, so read operations are thread-safe and
* multi-threading, whilst write operations are single threaded.
*
* This map has two backing maps, a primary one which contains established data. This is never locked for read and never updated directly, so can always
* multi-thread. The second backing map contains recent updates and is periodically flushed to the primary map. A mapping for a given key only exists in at most
* one of the backing maps, which means that some write operations force a flush of the secondary map.
*
*
This map is appropriate for use in situations where updates tend to come in batches, for example associated with the activation of a new publish version.
*
*
Note that this map implementation does not support mutation through its entry set, key set or values collection, nor the iterators upon them. If you need
* this functionality, copy this map into a new one, mutate that and then use the replace(java.util.Map) method to update this map.
*
* @param the key type for this map
* @param the value type for this map
*
* @author Simon Greatrix
*/
@SuppressWarnings("SuspiciousMethodCalls")
public class DeferredWriteMap implements ConcurrentMap {
/**
* Value representing null values inside tables.
*/
protected static final Object NULL_VAL = new Object();
/** How often the dynamic data is flushed. */
private static final long DEFAULT_FLUSH_INTERVAL = 600000;
/**
* Add all the contents of the source map to the destination, masking null values as appropriate.
*
* @param dest destination map
* @param src source map
*/
private static void doPutAll(Map dest, Map src) {
if (src == null) {
return;
}
for (Entry e : src.entrySet()) {
dest.put(e.getKey(), maskNull(e.getValue()));
}
}
/**
* Returns internal representation for value. Use NULL_VAL if value is null.
*
* @param key key to mask if it is null
* @param the type which is being masked
*
* @return the masked key
*/
protected static Object maskNull(T key) {
return key == null ? NULL_VAL : key;
}
/**
* Returns value represented by specified internal representation.
*
* @param key key to unmask if should be null
* @param the type which is being unmasked
*
* @return the unmasked key
*/
protected static T unmaskNull(Object key) {
if (key == NULL_VAL) {
return null;
}
@SuppressWarnings("unchecked")
T t = (T) key;
return t;
}
/** Implementation of Map.Entry used in the entry set. */
private class MyEntry implements Entry {
/** Key for this entry. */
private final K key;
MyEntry(K key) {
this.key = key;
}
/** {@inheritDoc} */
public K getKey() {
return key;
}
/** {@inheritDoc} */
public V getValue() {
return get(key);
}
/** {@inheritDoc} */
public V setValue(V value) {
return put(key, value);
}
}
/** Dynamic Map loaded with changes. */
protected final Map dynamicMap;
/** Time between dynamic map flushes. */
protected final long flushInterval;
/** Time of nextEntry map flush. */
protected long nextFlush = 0;
/**
* Static Map periodically updated with dynamic additions.
*/
protected Map staticMap;
/**
* Create new instance.
*
* @param init initialisation data which is copied into this, may be null
* @param dynamicMap backing dynamic map.
* @param flushInterval milliseconds between flushes
*/
protected DeferredWriteMap(Map init, Map dynamicMap, long flushInterval) {
if (init == null) {
staticMap = newMap(-1);
} else {
staticMap = newMap(init.size());
doPutAll(staticMap, init);
}
this.dynamicMap = dynamicMap;
this.flushInterval = flushInterval;
}
/**
* Create a new instance with the default flush interval and backed by a java.util.HashMap
*/
public DeferredWriteMap() {
this(null, new HashMap<>(), DEFAULT_FLUSH_INTERVAL);
}
/**
* Create a new instance backed by the specified dynamic map and flushing at the specified interval.
*
* @param flushInterval milliseconds between flushes
*/
public DeferredWriteMap(long flushInterval) {
this(null, new HashMap<>(), flushInterval);
}
/**
* Create a new instance with the default flush interval and backed by a java.util.HashMap
*
* @param map map to copy data from on initialisation
*/
public DeferredWriteMap(Map map) {
this(map, new HashMap<>(), DEFAULT_FLUSH_INTERVAL);
}
/**
* Create a new instance backed by the specified dynamic map and flushing at the specified interval.
*
* @param map map to copy data from on initialisation
* @param flushInterval milliseconds between flushes
*/
public DeferredWriteMap(Map map, long flushInterval) {
this(map, new HashMap<>(), flushInterval);
}
/** {@inheritDoc} */
public void clear() {
synchronized (dynamicMap) {
staticMap = newMap(-1);
dynamicMap.clear();
}
}
/** {@inheritDoc} */
public boolean containsKey(Object key) {
if (staticMap.containsKey(key)) {
return true;
}
// It is possible that the key is currently in the dynamic map. It may
// get flushed down to the static map before we get the lock, so we
// need to recheck.
synchronized (dynamicMap) {
if (dynamicMap.containsKey(key)) {
return true;
}
if (staticMap.containsKey(key)) {
return true;
}
}
return false;
}
/** {@inheritDoc} */
public boolean containsValue(Object value) {
if (staticMap.containsValue(value)) {
return true;
}
// It is possible that the value is currently in the dynamic map. It
// may get flushed down to the static map before we get the lock, so
// we need to recheck.
synchronized (dynamicMap) {
if (dynamicMap.containsValue(value)) {
return true;
}
if (staticMap.containsValue(value)) {
return true;
}
}
return false;
}
/**
* Get the entries in this map. The map cannot be removed from or added to via the returned set, and similarly additions and removals from this map are not
* reflected in the returned set. However the entry's getValue() method always returns the current mapping (which will be null if the mapping has been
* removed) and the entry's setValue method will write through to this map.
*
* @return read-only set of all entries in this map
*
* @see Map#entrySet()
*/
@Nonnull
public Set> entrySet() {
flushDynamic(true);
Set inEntries = staticMap.keySet();
Set> entries = new HashSet<>(1 + (int) Math.ceil(inEntries.size() / 0.75), 0.75f);
for (K inEntry : inEntries) {
entries.add(new MyEntry(inEntry));
}
return Collections.unmodifiableSet(entries);
}
@Override
public boolean equals(Object o) {
flushDynamic(true);
return staticMap.equals(o);
}
/**
* Copy all the entries from the dynamic map to the static map if a reasonable amount of time has passed since the last flush.
*
* @param force if true update the static map regardless of timing
*/
protected void flushDynamic(boolean force) {
long now = System.currentTimeMillis();
synchronized (dynamicMap) {
if (dynamicMap.isEmpty()) {
return;
}
if ((!force) && (nextFlush > now)) {
return;
}
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = now + flushInterval;
}
}
/** {@inheritDoc} */
public V get(Object key) {
Object v = staticMap.get(key);
if (v == null) {
synchronized (dynamicMap) {
v = dynamicMap.get(key);
if (v == null) {
v = staticMap.get(key);
} else {
flushDynamic(false);
}
}
}
return unmaskNull(v);
}
@Override
public int hashCode() {
flushDynamic(true);
return staticMap.hashCode();
}
/** {@inheritDoc} */
public boolean isEmpty() {
if (!staticMap.isEmpty()) {
return false;
}
synchronized (dynamicMap) {
if (!dynamicMap.isEmpty()) {
return false;
}
return staticMap.isEmpty();
}
}
/**
* Get the set of all keys for this map. The map cannot be updated through this set nor through iterators upon it.
*
* @return set of all keys in this map.
*
* @see Map#keySet()
*/
@Override
@Nonnull
public Set keySet() {
flushDynamic(true);
Set inKeys = staticMap.keySet();
Set entries = new HashSet<>(1 + (int) Math.ceil(inKeys.size() / 0.75), 0.75f);
entries.addAll(inKeys);
return Collections.unmodifiableSet(entries);
}
/**
* Create a new map for the static data with the specified size. This implementation returns an instance of a java.util.HashMap.
*
* @param size required capacity. If non-positive, then used a default capacity.
*
* @return the new map
*/
protected Map newMap(int size) {
if (size <= 0) {
return new HashMap<>();
}
// 0.75 if the default load factor for hash maps
return new HashMap<>((int) Math.ceil(size / 0.75f), 0.75f);
}
/** {@inheritDoc} */
public V put(K key, V value) {
Object v = maskNull(value);
synchronized (dynamicMap) {
Object oldStaticVal = staticMap.get(key);
Object oldVal = dynamicMap.put(key, v);
if (oldStaticVal != null) {
// the value was in static, so we must flush now
oldVal = oldStaticVal;
flushDynamic(true);
} else {
// Heuristically detect if this is the first of a new batch of
// updates, such as might occur after a publish.
long now = System.currentTimeMillis();
if (nextFlush + flushInterval < now) {
// We guess this is part of a new batch, so set the nextEntry
// flush time into the future to give some time for the
// rest of the batch to arrive.
nextFlush = now + flushInterval;
}
}
return unmaskNull(oldVal);
}
}
/** {@inheritDoc} */
@Override
public void putAll(@Nullable Map extends K, ? extends V> t) {
synchronized (dynamicMap) {
doPutAll(dynamicMap, t);
flushDynamic(true);
}
}
/** {@inheritDoc} */
public V putIfAbsent(@Nullable K key, V value) {
// check static map first for speed
Object oldVal = staticMap.get(key);
if (oldVal != null) {
return unmaskNull(oldVal);
}
synchronized (dynamicMap) {
// wasn't in static, but might be in dynamic
oldVal = dynamicMap.get(key);
if (oldVal != null) {
return unmaskNull(oldVal);
}
// small chance of a flush, so recheck static
oldVal = staticMap.get(key);
if (oldVal != null) {
return unmaskNull(oldVal);
}
// definitely absent so we can just add it to dynamic
// and we are done.
dynamicMap.put(key, value);
// Heuristically detect if this is the first of a new batch of
// updates, such as might occur after a publish.
long now = System.currentTimeMillis();
if (nextFlush + flushInterval < now) {
// We guess this is part of a new batch, so set the nextEntry
// flush time into the future to give some time for the
// rest of the batch to arrive.
nextFlush = now + flushInterval;
}
// the old value must have been null
return null;
}
}
/** {@inheritDoc} */
public V remove(@Nullable Object key) {
synchronized (dynamicMap) {
if (staticMap.containsKey(key)) {
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
final Object val = newDict.remove(key);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
return unmaskNull(val);
}
return unmaskNull(dynamicMap.remove(key));
}
}
/** {@inheritDoc} */
public boolean remove(@Nullable Object key, @Nullable Object value) {
Object val = maskNull(value);
synchronized (dynamicMap) {
// check static map
Object oldVal = staticMap.get(key);
if (oldVal == null) {
// may be in dynamic map, but if missing cannot remove
oldVal = dynamicMap.get(key);
if ((oldVal != null) && oldVal.equals(val)) {
// found in dynamic remove
dynamicMap.remove(key);
return true;
}
// was missing or didn't match
return false;
}
// check if we have the right value for removing
if (!oldVal.equals(val)) {
return false;
}
// found match in static, so will have to rebuild
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
newDict.remove(key);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
return true;
}
}
/**
* Remove mappings from the map. Mappings are only removed if they are identical to those in the map.
*
* @param map the key-value pairs to remove.
*
* @return the number of entries removed
*/
public int removeAll(Map map) {
int cnt = 0;
synchronized (dynamicMap) {
// create map of everything in this map
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
// loop over provided map...
for (Entry e : map.entrySet()) {
Object k = e.getKey();
Object v = maskNull(e.getValue());
Object c = newDict.get(k);
// if entry matches the one in this map, remove it
if (v.equals(c)) {
newDict.remove(k);
cnt++;
}
}
// update our static map
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
}
return cnt;
}
/**
* Remove a set of key mappings from the map.
*
* @param keys the set of keys to remove
*
* @return the number of entries removed
*/
public int removeAll(Set> keys) {
int cnt = 0;
synchronized (dynamicMap) {
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
for (Object k : keys) {
if (newDict.remove(k) != null) {
cnt++;
}
}
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
}
return cnt;
}
/** {@inheritDoc} */
public V replace(@Nullable K key, @Nullable V value) {
Object val = maskNull(value);
synchronized (dynamicMap) {
// check static map. As we have the lock, it will be in static or
// in dynamic but not in both.
Object oldStaticVal = staticMap.get(key);
if (oldStaticVal == null) {
// may be in dynamic map, but if missing cannot replace
Object oldVal = dynamicMap.get(key);
if (oldVal == null) {
return null;
}
// not in static map, so only need to update dynamic
return unmaskNull(dynamicMap.put(key, val));
}
// value was mapped in static, so need to update and flush
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
newDict.put(key, val);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
return unmaskNull(oldStaticVal);
}
}
/** {@inheritDoc} */
public boolean replace(@Nullable K key, @Nullable V oldValue, @Nullable V newValue) {
Object oldVal = maskNull(oldValue);
Object newVal = maskNull(newValue);
if (oldVal.equals(newVal)) {
return false;
}
synchronized (dynamicMap) {
// check static map. As we have the lock, it will be in static or
// in dynamic but not in both.
Object currVal = staticMap.get(key);
if (currVal == null) {
// may be in dynamic map, but if missing cannot replace
currVal = dynamicMap.get(key);
if (currVal == null) {
return false;
}
// not in static map, so only need to update replace if value
// matches
if (currVal.equals(oldVal)) {
// value matches, do replace
dynamicMap.put(key, newVal);
return true;
}
// was mapped but to a different val, so no replace
return false;
}
// value was mapped in static, did it match?
if (!currVal.equals(oldVal)) {
return false;
}
// mapped in static and matches value, so do replace
Map newDict = newMap(staticMap.size() + dynamicMap.size());
newDict.putAll(staticMap);
newDict.putAll(dynamicMap);
newDict.put(key, newVal);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
return true;
}
}
/**
* Replace the contents of this map with the same mapping as in the supplied map.
*
* @param map replacement mappings
*/
public void replace(Map map) {
synchronized (dynamicMap) {
Map newDict = newMap(map.size());
doPutAll(newDict, map);
staticMap = Collections.unmodifiableMap(newDict);
dynamicMap.clear();
nextFlush = System.currentTimeMillis() + flushInterval;
}
}
/** {@inheritDoc} */
public int size() {
flushDynamic(true);
return staticMap.size();
}
@Override
public String toString() {
flushDynamic(true);
return staticMap.toString();
}
/**
* Get the collection of all values in this map. The map cannot be updated through this collection nor through iterators upon it.
*
* @return collection all values in this map
*
* @see Map#values()
*/
@Nonnull
public Collection values() {
flushDynamic(true);
Collection