/*
* #%L
* Nazgul Project: nazgul-core-cache-impl-inmemory
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* 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%
*
*/
package se.jguru.nazgul.core.cache.impl.inmemory;
import se.jguru.nazgul.core.algorithms.api.Validate;
import se.jguru.nazgul.core.cache.api.Cache;
import se.jguru.nazgul.core.cache.api.CacheListener;
import se.jguru.nazgul.core.cache.api.transaction.TransactedAction;
import se.jguru.nazgul.core.clustering.api.AbstractSwiftClusterable;
import se.jguru.nazgul.core.clustering.api.IdGenerator;
import se.jguru.nazgul.core.clustering.api.UUIDGenerator;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Abstract Map-backed implementation of the Cache interface.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
public class InMemoryMapCache extends AbstractSwiftClusterable implements Cache<String, Serializable> {
// Internal state
private long timeoutMillis;
private ConcurrentMap<String, Serializable> cache;
private ConcurrentMap<String, CacheListener<String, Serializable>> listeners;
private boolean fakeTransactions;
private String threadPoolPrefix;
private int numEventListenerThreads;
private transient ExecutorService listenerThreadService;
/**
* Default convenience constructor, using an UUIDGenerator, 20 minutes timeout,
* empty ConcurrentHashMaps for cache and listener maps, 15 threads in the pool
* and not faking transactedActions.
*/
public InMemoryMapCache() {
this(UUIDGenerator.getInstance(), 20 * 60 * 60L,
new ConcurrentHashMap<String, Serializable>(),
new ConcurrentHashMap<String, CacheListener<String, Serializable>>(),
15,
false);
}
/**
* Creates a new AbstractIdentifiable and assigns the internal ID state.
*
* @param idGenerator The ID generator used to acquire a cluster-unique
* identifier for this AbstractClusterable instance.
* @param timeoutMillis The cache element timeout in milliseconds.
* A value of zero implies no timeout/autonomous eviction.
* @param cache The map used for caching instances.
* @param listeners The map used for storing cache listeners.
* @param fakeTransactions if {@code true}, this InMemoryMapCache does not throw an UnsupportedOperationException
* when requested to perform transacted actions.
*/
public InMemoryMapCache(final IdGenerator idGenerator,
@Min(value = 1) final long timeoutMillis,
@NotNull final ConcurrentMap<String, Serializable> cache,
@NotNull final ConcurrentMap<String, CacheListener<String, Serializable>> listeners,
final int numEventListenerThreads,
final boolean fakeTransactions) {
super(idGenerator, false);
// Check sanity
Validate.isTrue(timeoutMillis > 0, "Cannot handle zero or negative milliseconds argument.");
Validate.notNull(cache, "cache");
Validate.notNull(listeners, "listeners");
// Assign internal state
this.timeoutMillis = timeoutMillis;
this.cache = cache;
this.listeners = listeners;
this.numEventListenerThreads = numEventListenerThreads;
this.threadPoolPrefix = "InMemoryCacheListener-(" + hashCode() + ")";
listenerThreadService = Executors.newFixedThreadPool(
numEventListenerThreads, new NamedSequenceThreadFactory(threadPoolPrefix));
this.fakeTransactions = fakeTransactions;
}
/**
* {@inheritDoc}
*/
@Override
public Serializable remove(final String key) {
// Remove the value
final Serializable oldValue = cache.remove(key);
// Notify any listeners
if (!listeners.isEmpty()) {
final Set<CacheListener<String, Serializable>> localListeners
= new HashSet<CacheListener<String, Serializable>>(listeners.values());
listenerThreadService.execute(
new CacheListenerNotificationWorker(localListeners, CacheEventType.REMOVE, key, oldValue, null));
}
// All done.
return oldValue;
}
/**
* {@inheritDoc}
*/
@Override
public Serializable put(final String key, final Serializable value) {
// Put the value in the cache
final Serializable oldValue = cache.put(key, value);
// Notify any listeners
if (!listeners.isEmpty()) {
final Set<CacheListener<String, Serializable>> localListeners
= new HashSet<CacheListener<String, Serializable>>(listeners.values());
final CacheEventType cacheEventType = oldValue == null ? CacheEventType.PUT : CacheEventType.UPDATE;
listenerThreadService.execute(
new CacheListenerNotificationWorker(localListeners, cacheEventType, key, oldValue, value));
}
// All done.
return oldValue;
}
/**
* {@inheritDoc}
*/
@Override
public Serializable get(final String key) {
return cache.get(key);
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsKey(final String key) {
return cache.containsKey(key);
}
/**
* Returns an iterator to a readonly version of the cache map.
*
* @return an Iterator to a readonly version of the cache map.
*/
@Override
public Iterator<String> iterator() {
return Collections.unmodifiableMap(cache).keySet().iterator();
}
/**
* {@inheritDoc}
*/
@Override
public boolean addListener(final CacheListener<String, Serializable> listener) {
final CacheListener<String, Serializable> putListener
= listeners.putIfAbsent(listener.getClusterId(), listener);
return putListener == listener;
}
/**
* Retrieves an unmodifiable list of all listener IDs.
* {@inheritDoc}
*/
@Override
public List<String> getListenerIds() {
return Collections.unmodifiableList(new ArrayList<String>(listeners.keySet()));
}
/**
* {@inheritDoc}
*/
@Override
public void removeListener(final String key) {
listeners.remove(key);
}
/**
* {@inheritDoc}
*/
@Override
protected void performWriteExternal(final ObjectOutput out) throws IOException {
// Write out state
out.writeLong(timeoutMillis);
out.writeBoolean(fakeTransactions);
out.writeInt(numEventListenerThreads);
out.writeUTF(threadPoolPrefix);
out.writeObject(listeners);
out.writeObject(cache);
}
/**
* {@inheritDoc}
*/
@Override
protected void performReadExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
// Read in state
timeoutMillis = in.readLong();
fakeTransactions = in.readBoolean();
numEventListenerThreads = in.readInt();
threadPoolPrefix = in.readUTF();
listeners = (ConcurrentMap<String, CacheListener<String, Serializable>>) in.readObject();
cache = (ConcurrentMap<String, Serializable>) in.readObject();
// Re-create the (transient) ExecutorService
this.listenerThreadService = Executors.newFixedThreadPool(
numEventListenerThreads, new NamedSequenceThreadFactory(threadPoolPrefix));
}
/**
* Acquires a Transactional context from this Cache, and Executes the
* TransactedAction::doInTransaction method within it.
*
* @param action The TransactedAction to be executed within a Cache Transactional context.
* @throws UnsupportedOperationException if the underlying Cache implementation does not
* support Transactions.
*/
@Override
public void performTransactedAction(final TransactedAction action) throws UnsupportedOperationException {
if (fakeTransactions) {
action.doInTransaction();
} else {
throw new UnsupportedOperationException("InMemoryMapCaches do not support transactions. Use "
+ "the fakeTransactions initialization parameter to simulate transacted behaviour.");
}
}
//
// Helpers
//
class CacheListenerNotificationWorker implements Runnable {
// Internal state
private Set<CacheListener<String, Serializable>> listeners;
private CacheEventType event;
private String key;
private Serializable oldValue;
private Serializable newValue;
/**
* Creates a CacheListenerNotificationWorker runnable which is used by the ExecutorService to perform
* asynchronous callbacks to cache listeners.
*
* @param listeners The set of listeners which should be notified.
* @param event The event type generated.
* @param key The cache key.
* @param oldValue The old value of the cache entry.
* @param newValue The new value of the cache entry.
*/
CacheListenerNotificationWorker(
@NotNull @Size(min = 1) final Set<CacheListener<String, Serializable>> listeners,
@NotNull final CacheEventType event,
@NotNull final String key,
final Serializable oldValue,
final Serializable newValue) {
super();
// Check sanity
Validate.notNull(event, "event");
Validate.notEmpty(listeners, "listeners");
Validate.notEmpty(key, "key");
// Assign internal state
this.listeners = listeners;
this.event = event;
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("all")
public void run() {
for (CacheListener<String, Serializable> current : listeners) {
switch (event) {
case PUT:
current.onPut(key, newValue);
break;
case UPDATE:
current.onUpdate(key, newValue, oldValue);
break;
case REMOVE:
current.onRemove(key, oldValue);
break;
case CLEAR:
current.onClear();
break;
case AUTONOMOUS_EVICT:
current.onAutonomousEvict(key, newValue);
break;
case AUTONOMOUS_LOAD:
current.onAutonomousLoad(key, newValue);
break;
default:
throw new IllegalStateException("Could not identify cache event [" + event + "]");
}
}
}
}
}