/*
* Copyright 2015 Ben Manes. All Rights Reserved.
*
* 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.github.benmanes.caffeine.jcache.event;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.cache.Cache;
import javax.cache.configuration.CacheEntryListenerConfiguration;
import javax.cache.event.CacheEntryEventFilter;
import javax.cache.event.CacheEntryListener;
import javax.cache.event.EventType;
/**
* A dispatcher that publishes cache events to listeners for asynchronous execution.
* <p>
* A {@link CacheEntryListener} is required to receive events in the order of the actions being
* performed on the associated key. This implementation supports this through an actor-like model
* by using a dispatch queue per listener. A listener is never executed in parallel on different
* events, but may be executed sequentially on different threads. Batch processing of the dispatch
* queue is not presently supported.
* <p>
* Some listeners may be configured as <tt>synchronous</tt>, meaning that the publishing thread
* should wait until the listener has processed the event. The calling thread should publish within
* an atomic block that mutates the entry, and complete the operation by calling
* {@link #awaitSynchronous()} or {@link #ignoreSynchronous()}.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class EventDispatcher<K, V> {
static final Logger logger = Logger.getLogger(EventDispatcher.class.getName());
static final ThreadLocal<List<CompletableFuture<Void>>> pending =
ThreadLocal.withInitial(ArrayList::new);
final Executor exectuor;
final Map<Registration<K, V>, CompletableFuture<Void>> dispatchQueues;
public EventDispatcher(Executor exectuor) {
this.dispatchQueues = new ConcurrentHashMap<>();
this.exectuor = requireNonNull(exectuor);
}
/**
* Registers a cache entry listener based on the supplied configuration.
*
* @param configuration the listener's configuration.
*/
public void register(CacheEntryListenerConfiguration<K, V> configuration) {
if (configuration.getCacheEntryListenerFactory() == null) {
return;
}
EventTypeAwareListener<K, V> listener = new EventTypeAwareListener<>(
configuration.getCacheEntryListenerFactory().create());
CacheEntryEventFilter<K, V> filter = event -> true;
if (configuration.getCacheEntryEventFilterFactory() != null) {
filter = new EventTypeFilter<>(listener,
configuration.getCacheEntryEventFilterFactory().create());
}
Registration<K, V> registration = new Registration<>(configuration, filter, listener);
dispatchQueues.putIfAbsent(registration, CompletableFuture.completedFuture(null));
}
/**
* Deregisters a cache entry listener based on the supplied configuration.
*
* @param configuration the listener's configuration.
*/
public void deregister(CacheEntryListenerConfiguration<K, V> configuration) {
requireNonNull(configuration);
dispatchQueues.keySet().removeIf(registration ->
configuration.equals(registration.getConfiguration()));
}
/**
* Publishes a creation event for the entry to all of the interested listeners.
*
* @param cache the cache where the entry was created
* @param key the entry's key
* @param value the entry's value
*/
public void publishCreated(Cache<K, V> cache, K key, V value) {
publish(cache, EventType.CREATED, key, /* newValue */ null, value, /* quiet */ false);
}
/**
* Publishes a update event for the entry to all of the interested listeners.
*
* @param cache the cache where the entry was updated
* @param key the entry's key
* @param oldValue the entry's old value
* @param newValue the entry's new value
*/
public void publishUpdated(Cache<K, V> cache, K key, V oldValue, V newValue) {
publish(cache, EventType.UPDATED, key, oldValue, newValue, /* quiet */ false);
}
/**
* Publishes a remove event for the entry to all of the interested listeners.
*
* @param cache the cache where the entry was removed
* @param key the entry's key
* @param value the entry's value
*/
public void publishRemoved(Cache<K, V> cache, K key, V value) {
publish(cache, EventType.REMOVED, key, /* oldValue */ null, value, /* quiet */ false);
}
/**
* Publishes a remove event for the entry to all of the interested listeners. This method does
* not register the synchronous listener's future with {@link #awaitSynchronous()}.
*
* @param cache the cache where the entry was removed
* @param key the entry's key
* @param value the entry's value
*/
public void publishRemovedQuietly(Cache<K, V> cache, K key, V value) {
publish(cache, EventType.REMOVED, key, /* oldValue */ null, value, /* quiet */ true);
}
/**
* Publishes a expire event for the entry to all of the interested listeners.
*
* @param cache the cache where the entry expired
* @param key the entry's key
* @param value the entry's value
*/
public void publishExpired(Cache<K, V> cache, K key, V value) {
publish(cache, EventType.EXPIRED, key, value, /* newValue */ null, /* quiet */ false);
}
/**
* Publishes a expire event for the entry to all of the interested listeners. This method does
* not register the synchronous listener's future with {@link #awaitSynchronous()}.
*
* @param cache the cache where the entry expired
* @param key the entry's key
* @param value the entry's value
*/
public void publishExpiredQuietly(Cache<K, V> cache, K key, V value) {
publish(cache, EventType.EXPIRED, key, value, /* newValue */ null, /* quiet */ true);
}
/**
* Blocks until all of the synchronous listeners have finished processing the events this thread
* published.
*/
public void awaitSynchronous() {
List<CompletableFuture<Void>> futures = pending.get();
if (futures.isEmpty()) {
return;
}
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
} catch (CompletionException e) {
logger.log(Level.WARNING, null, e);
} finally {
futures.clear();
}
}
/**
* Ignores and clears the queued futures to the synchronous listeners that are processing events
* this thread published.
*/
public void ignoreSynchronous() {
pending.get().clear();
}
/** Broadcasts the event to all of the interested listener's dispatch queues. */
private void publish(Cache<K, V> cache, EventType eventType,
K key, V oldValue, V newValue, boolean quiet) {
if (dispatchQueues.isEmpty()) {
return;
}
JCacheEntryEvent<K, V> event = null;
for (Registration<K, V> registration : dispatchQueues.keySet()) {
if (!registration.getCacheEntryListener().isCompatible(eventType)) {
continue;
}
if (event == null) {
event = new JCacheEntryEvent<>(cache, eventType, key, oldValue, newValue);
}
if (!registration.getCacheEntryFilter().evaluate(event)) {
continue;
}
JCacheEntryEvent<K, V> e = event;
CompletableFuture<Void> future =
dispatchQueues.computeIfPresent(registration, (k, queue) -> {
Runnable action = () -> registration.getCacheEntryListener().dispatch(e);
return queue.thenRunAsync(action, exectuor);
});
if ((future != null) && registration.isSynchronous() && !quiet) {
pending.get().add(future);
}
}
}
}