/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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.
*/
package com.cinchapi.concourse.util;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.cinchapi.concourse.annotate.Experimental;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
/**
* An AutoMap automatically and atomically manages entry creation and removal
* using provided {@code loader} and {@code cleaner} functions. This is
* especially useful if you have a map that associates a key to another
* collection and you want to be able to access the mapped collection without
* first checking if it exist and you want empty collections to be removed
* nearly on the fly without explicitly checking.
* <p>
*
* Using an AutoMap you can replace
*
* <pre>
* V value = map.get(key);
* if(value == null) {
* value = load(value);
* map.put(key, value);
* }
* </pre>
*
* with
*
* <pre>
* V value = map.get(key);
* </pre>
*
* and be sure that {@code value} is not {@code null} because it has been
* previously set or atomically loaded and set with the provided {@code loader}
* function.
* </p>
* <p>
* Additionally, you never have to worry about removing entries from the map or
* doing any kind of cleanup based on value conditions similar to
*
* <pre>
* V value = map.get(key);
* modify(value);
* if(satisifiesCondition(value)) {
* map.remove(key);
* }
* </pre>
*
* because a background thread periodically goes through the map and atomically
* cleans up eligible entries. The background thread uses local synchronization
* on entries so it never interferes with overall map operations.
* </p>
*
* @author Jeff Nelson
*/
@Experimental
public abstract class AutoMap<K, V> extends AbstractMap<K, V> {
/**
* Return an {@link AutoMap} that is a drop in replacement for a
* {@link HashMap}.
*
* @param loader
* @param cleaner
* @return the AutoMap
*/
public static <K, V> AutoHashMap<K, V> newAutoHashMap(
Function<K, V> loader, Function<V, Boolean> cleaner) {
return new AutoHashMap<K, V>(loader, cleaner);
}
/**
* Return an {@link AutoMap} that is a drop in replacement for a
* {@link TreeMap}.
*
* @param loader
* @param cleaner
* @return the AutoMap
*/
public static <K extends Comparable<K>, V> AutoSkipListMap<K, V> newAutoSkipListMap(
Function<K, V> loader, Function<V, Boolean> cleaner) {
return new AutoSkipListMap<K, V>(loader, cleaner);
}
/**
* Set the amount of time between each cleanup run. Changing these values
* won't affect existing AutoMap instances, but subsequent creations will
* reflect the updated values.
*
* @param delay
* @param unit
*/
// --- visible for testing
protected static void setCleanupDelay(long delay, TimeUnit unit) {
CLEANUP_DELAY = delay;
CLEANUP_DELAY_UNIT = unit;
}
/**
* This method is provided to access {@link #CLEANUP_DELAY} because the
* value may change during testing if
* {@link #setCleanupDelay(long, TimeUnit)}. is called.
*
* @return {@link #CLEANUP_DELAY}.
*/
private static long getCleanupDelay() {
return CLEANUP_DELAY;
}
/**
* This method is provided to access {@link #CLEANUP_DELAY_UNIT} because the
* value may change during testing if
* {@link #setCleanupDelay(long, TimeUnit)}. is called.
*
* @return {@link #CLEANUP_DELAY_UNIT}.
*/
private static TimeUnit getCleanupDelayUnit() {
return CLEANUP_DELAY_UNIT;
}
/**
* The duration of time measured in {@link #CLEANUP_DELAY_UNIT} that the
* {@link #cleanupThread} will sleep between cleans.
*/
private static long CLEANUP_DELAY = 60;
/**
* The time unit for {@link #CLEANUP_DELAY}.
*/
private static TimeUnit CLEANUP_DELAY_UNIT = TimeUnit.SECONDS;
/**
* The map that serves as the backingStore for the data managed by this
* class.
*/
protected final Map<K, V> backingStore;
/**
* The caller defined loader function.
*/
private final Function<K, V> loader;
/**
* The caller defined cleaner function.
*/
private final Function<V, Boolean> cleaner;
/**
* A background thread that iterates through the map and cleans up eligible
* entries from time to time.
*/
private final Thread cleanupThread = new Thread() {
{
setDaemon(true);
}
@Override
public void run() {
while (true) {
for (Entry<K, V> entry : backingStore.entrySet()) {
synchronized (entry) {
if(cleaner.apply(entry.getValue())) {
backingStore.remove(entry.getKey());
}
}
}
try {
getCleanupDelayUnit().sleep(getCleanupDelay());
}
catch (InterruptedException e) {
throw Throwables.propagate(e);
}
}
}
};
/**
* Construct a new instance.
*
* @param backingStore
* @param loader
* @param cleaner
*/
protected AutoMap(Map<K, V> backingStore, Function<K, V> loader,
Function<V, Boolean> cleaner) {
this.backingStore = backingStore;
this.loader = loader;
this.cleaner = cleaner;
cleanupThread.start();
}
@Override
public Set<Entry<K, V>> entrySet() {
return backingStore.entrySet();
}
/**
* Return the value mapped from {@code key} if it exists, otherwise, load
* the value using the provided loader function.
*/
@Override
@SuppressWarnings("unchecked")
public V get(Object key) {
synchronized (key) {
V value = backingStore.get(key);
if(value == null) {
value = loader.apply((K) key);
backingStore.put((K) key, value);
}
return value;
}
}
}