/********************************************************************************* * Copyright 2015-present trivago GmbH * * 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.trivago.triava.tcache.event; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import javax.cache.configuration.CacheEntryListenerConfiguration; import javax.cache.configuration.Factory; import javax.cache.event.CacheEntryCreatedListener; import javax.cache.event.CacheEntryEvent; import javax.cache.event.CacheEntryEventFilter; import javax.cache.event.CacheEntryExpiredListener; import javax.cache.event.CacheEntryListener; import javax.cache.event.CacheEntryRemovedListener; import javax.cache.event.CacheEntryUpdatedListener; import javax.cache.event.EventType; import com.trivago.triava.tcache.Cache; /** * Holds a CacheEntryListenerConfiguration, and the objects created from it: CacheEntryEventFilter and * CacheEntryListener. The {@link #hashCode()} and {@link #equals(Object)} are only looking whether the * CacheEntryListenerConfiguration are the identical object (reference comparison), which makes it easy to * follow the JSR107 requirement that the same CacheEntryListenerConfiguration must be registered only once * via {@link javax.cache.Cache#registerCacheEntryListener(CacheEntryListenerConfiguration)}. * <p> * This class is strictly internal: Fields should be private and this class should provide only * package-private methods. * * @author cesken * * @param <K> The Key type * @param <V> The Value type */ final class ListenerEntry<K,V> { private final CacheEntryListenerConfiguration<K, V> config; private CacheEntryEventFilter<? super K, ? super V> filter = null; private CacheEntryListener<? super K, ? super V> listener = null; private final Cache<K, V> tcache; private final CacheEventManager<K,V> eventManager; private final DispatchMode dispatchMode; private final BlockingQueue<TCacheEntryEventCollection<K,V>> dispatchQueue; private DispatchRunnable dispatchThread = null; /** * Creates a ListenerEntry from the factories in CacheEntryListenerConfiguration. * Both CacheEntryEventFilter and CacheEntryListener are created. * The {@link #dispatchMode} regulates how events get dispatched, for example synchronous, asynchronous batched or timed * * @param config The CacheEntryListenerConfiguration * @param tcache The cache which events should be listened to * @param dispatchMode How events are dispatched to listeners */ ListenerEntry(CacheEntryListenerConfiguration<K, V> config, Cache<K,V> tcache, DispatchMode dispatchMode) { this.config = config; this.tcache = tcache; this.dispatchMode = dispatchMode; CacheEventManager<K,V> em = null; Factory<CacheEntryListener<? super K, ? super V>> listenerFactory = config.getCacheEntryListenerFactory(); if (listenerFactory != null) { listener = listenerFactory.create(); if (listener != null) { Factory<CacheEntryEventFilter<? super K, ? super V>> filterFactory = config .getCacheEntryEventFilterFactory(); if (filterFactory != null) filter = filterFactory.create(); em = new ListenerCacheEventManager<K,V>(); } } eventManager = em; /** * Initialize dispatchQueue and corresponding Thread. This has to be done even for the synchronous * DispatchMode#SYNC, as it can be forced to operate asynchronously for internal operations like * expiration and eviction. */ this.dispatchQueue = new ArrayBlockingQueue<TCacheEntryEventCollection<K, V>>(1024); /** * Future directions: Starting the listener in the constructor is problematic. If this class would be * subclassed, the Thread would start too early. Right now it cannot happen, as this class is final. * Second, we possibly want a Thread restart mechanism anyhow, like we have with the expiration and * eviction threads. For the latter, there should be a dedicated "BackgroundThreadController<T>" class * that controls/restarts background threads. */ dispatchThread = ensureListenerThreadIsRunning(); } CacheEntryListenerConfiguration<K, V> getConfig() { return config; } /** * Sends the events to the listener, if it passes the filter. Sending is done in batches of up to 256 events, * either synchronously or asynchronously * All events in the list must have the same event type. * * @param events The events to dispatch * @param eventType The event type * @param forceAsync Force async mode */ void dispatch(Iterable<TCacheEntryEvent<K, V>> events, EventType eventType, boolean forceAsync) { if (eventManager == null) return; @SuppressWarnings("unchecked") CacheEntryListener<K, V> listenerRef = (CacheEntryListener<K, V>) this.listener; int batchSize = 256; int i = 0; // int sentCount = 0; // int sentBatches = 0; boolean needsSend = false; List<CacheEntryEvent<? extends K, ? extends V>> interestingEvents = new ArrayList<>(batchSize); for (TCacheEntryEvent<? extends K, ? extends V> event : events) { if (!interested(event)) continue; // filtered interestingEvents.add(event); needsSend = true; // sentCount ++; if (i++ == batchSize) { // sentBatches++; scheduleEvents(interestingEvents, listenerRef, eventType, forceAsync); needsSend = false; interestingEvents = new ArrayList<>(batchSize); i = 0; } } // Push out the last batch if (needsSend) { // sentBatches++; scheduleEvents(interestingEvents, listenerRef, eventType, forceAsync); } // if (sentBatches > 0) // System.out.println("sendEvent: " + sentCount + " in " + sentBatches + " batches (multi). type=" + eventType); } /** * Sends one event to the listener, if it passes the filter. Sending is either done synchronously or asynchronously * * @param event The event to dispatch */ void dispatch(TCacheEntryEvent<K, V> event) { if (eventManager == null) return; if (!interested(event)) return; // filtered @SuppressWarnings("unchecked") CacheEntryListener<K, V> listenerRef = (CacheEntryListener<K, V>) this.listener; if (!dispatchMode.isAsync()) { sendEvent(event, listenerRef); } else { try { dispatchQueue.put(createSingleEvent(event)); } catch (InterruptedException e) { /** Interruption policy: * The #dispatch method can be part of client interaction like a put or get call. Or it can * be from internal operations like eviction. In both cases we do not want to blindly * bubble up the stack until some random code catches it. Reason is, that it could leave the * Cache in an inconsistent state, e.g. a value was put() into the cache but the statistics * do not reflect that. Thus, we simply mark the current thread interrupted, so any caller * on any stack level may inspect the status. */ Thread.currentThread().interrupt(); // If we come here, the event may not be in the dispatchQueue. But we will not // retry, as there are no guarantees when interrupting and it is safer to just go on. // For example if during shutdown the dispatchQueue is full, we would iterate here // forever as the DispatchRunnable instance could be shutdown and not read from the // queue any longer. } } } /** * Schedules to send the events to the given listener. Scheduling means to send immediately if this * {@link ListenerEntry} is synchronous, or to put it in a queue if asynchronous (including the forceAsync * parameter. For synchronous delivery, it is guaranteed that the listener was executed when returning * from this method. * <p> * The given eventType must match all events * * @param events The events to send * @param listener The listener to notify * @param eventType The event type. It must match all events to send * @param forceAsync */ private void scheduleEvents(List<CacheEntryEvent<? extends K, ? extends V>> events, CacheEntryListener<K, V> listener, EventType eventType, boolean forceAsync) { if (eventManager == null) return; TCacheEntryEventCollection<K, V> eventColl = new TCacheEntryEventCollection<>(events, eventType); if (!(forceAsync || dispatchMode.isAsync())) { sendEvents(eventColl, listener); } else { try { dispatchQueue.put(eventColl); } catch (InterruptedException e) { /** Interruption policy: * The #dispatch method can be part of client interaction like a put or get call. Or it can * be from internal operations like eviction. In both cases we do not want to blindly * bubble up the stack until some random code catches it. Reason is, that it could leave the * Cache in an inconsistent state, e.g. a value was put() into the cache but the statistics * do not reflect that. Thus, we simply mark the current thread interrupted, so any caller * on any stack level may inspect the status. */ Thread.currentThread().interrupt(); // If we come here, the event may not be in the dispatchQueue. But we will not // retry, as there are no guarantees when interrupting and it is safer to just go on. // For example if during shutdown the dispatchQueue is full, we would iterate here // forever as the DispatchRunnable instance could be shutdown and not read from the // queue any longer. } } } private void sendEvent(CacheEntryEvent<? extends K, ? extends V> event, CacheEntryListener<K, V> listener) { // System.out.println("sendEvent: 1 (single)"); sendEvents(createSingleEvent(event), listener); } private void sendEvents(TCacheEntryEventCollection<K, V> eventColl, CacheEntryListener<K, V> listener) { EventType eventType = eventColl.eventType(); // System.out.println("sendEvents to listener " + listener + ", eventType=" + eventColl.eventType()); switch (eventType) { case CREATED: if (listener instanceof CacheEntryCreatedListener) eventManager.created((CacheEntryCreatedListener<K, V>)listener, eventColl); break; case EXPIRED: if (listener instanceof CacheEntryExpiredListener) eventManager.expired((CacheEntryExpiredListener<K, V>)listener, eventColl); break; case UPDATED: if (listener instanceof CacheEntryUpdatedListener) eventManager.updated((CacheEntryUpdatedListener<K,V>)listener, eventColl); break; case REMOVED: if (listener instanceof CacheEntryRemovedListener) eventManager.removed((CacheEntryRemovedListener<K,V>)listener, eventColl); break; default: // By default do nothing. If new event types are added to the Spec, they will be ignored. } } /** * Creates a TCacheEntryEventCollection from the given single event. * * @param event The event * @return The event collection with one element */ private TCacheEntryEventCollection<K,V> createSingleEvent(TCacheEntryEvent<K, V> event) { List<CacheEntryEvent<? extends K, ? extends V>> events = new ArrayList<>(); events.add(event); TCacheEntryEventCollection<K,V> coll = new TCacheEntryEventCollection<K,V>(events, event.getEventType()); return coll; } /** * Checks whether this ListenerEntry is interested in the given event. More formally, * it is interested, when it passses the filter or when there is no filter at all. * * @param event The CacheEntryEvent to check * @return true if interested */ private boolean interested(CacheEntryEvent<? extends K, ? extends V> event) { return filter == null ? true : filter.evaluate(event); } // public CacheEntryEventFilter<? super K, ? super V> getListener() // { // return listener; // } void shutdown() { DispatchRunnable runnable = dispatchThread; if (runnable != null) { runnable.shutdown(); } } @Override public int hashCode() { return getConfig().hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof ListenerEntry)) return false; return this.getConfig() == ((ListenerEntry<?,?>)obj).getConfig(); } /** * */ private class DispatchRunnable extends Thread implements Runnable { private volatile boolean running = true; DispatchRunnable(String cacheId) { super("tCache-Notifier:" + cacheId); setDaemon(true); } @Override public void run() { @SuppressWarnings("unchecked") CacheEntryListener<K, V> listenerRef = (CacheEntryListener<K, V>) listener; while (running) { try { TCacheEntryEventCollection<K, V> eventColl = dispatchQueue.take(); sendEvents(eventColl, listenerRef); } catch (InterruptedException ie) { // Interruption policy: Only used for quitting } catch (Exception exc) { // If the thread enters this line, there was an issue wit sendEvent(). Likely it // is in the user provided Listener code, so we must make sure not to die if this // happens. For now we will silently ignore any errors. } } } public void shutdown() { running = false; Thread thread = dispatchThread; if (thread != null) { thread.interrupt(); dispatchThread = null; } } } /** * Starts the DispatchRunnable thread * * @return */ private DispatchRunnable ensureListenerThreadIsRunning() { dispatchThread = new DispatchRunnable(tcache.id()); dispatchThread.start(); return dispatchThread; } /** * Creates an Iterable with a single element in it * @param event The event to make available * @return The Iterable */ private TCacheEntryEventCollection<K, V> createSingleEvent( CacheEntryEvent<? extends K, ? extends V> event) { List<CacheEntryEvent<? extends K, ? extends V>> list = new ArrayList<>(); list.add(event); TCacheEntryEventCollection<K,V> coll = new TCacheEntryEventCollection<>(list, event.getEventType()); return coll; } /** * Returns whether this {@link ListenerEntry} is listening to the given eventType * @param eventType The event Type * @return true, if the listener is listening to the given eventType */ boolean isListeningFor(EventType eventType) { switch (eventType) { case CREATED: return listener instanceof CacheEntryCreatedListener; case EXPIRED: return listener instanceof CacheEntryExpiredListener; case REMOVED: return listener instanceof CacheEntryRemovedListener; case UPDATED: return listener instanceof CacheEntryUpdatedListener; } return false; } }