package org.cache2k.jcache.provider.event; /* * #%L * cache2k JCache provider * %% * Copyright (C) 2000 - 2017 headissue GmbH, Munich * %% * 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. * #L% */ import javax.cache.configuration.CacheEntryListenerConfiguration; import javax.cache.event.EventType; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * Calls the listeners with help of the executor. The dispatcher is designed to * achieve maximum parallelism but deliver events for one key in the correct order. * * <p>General principle: Before a listener is called a queue is created for * the specific key. In the queue all following events will be enqueued in case the * listener isn't finished yet. * * <p>Rationale: We get get called with a new event synchronously and then do the * async dispatching. This means we need to be careful not to introduce contention * in the synchronous listener execution of the cache. Alternatively we could just * have add the event to a queue and then do the more complex dispatching logic in a * separate thread, however, that means that we would need an additional thread per cache. * * @author Jens Wilke */ public class AsyncDispatcher<K,V> { private static final int KEY_LOCKS_MASK = 2 << (31 - Integer.numberOfLeadingZeros(Runtime.getRuntime().availableProcessors())) - 1; private static Object[] KEY_LOCKS; static { KEY_LOCKS = new Object[KEY_LOCKS_MASK + 1]; for (int i = 0; i < KEY_LOCKS.length; i++) { KEY_LOCKS[i] = new Object(); } } /** * Simulate locking by key, use the hash code to spread and avoid lock contention. * The additional locking we introduce here is currently run synchronously inside the * entry mutation operation. */ static Object getLockObject(Object key) { return KEY_LOCKS[key.hashCode() & KEY_LOCKS_MASK]; } private Executor executor; /** * A hash map updated concurrently for events on different keys. For the queue we * use a non thread safe linked list, because only one operation happens per key * at once. */ private Map<K, Queue<EntryEvent<K, V>>> keyQueue = new ConcurrentHashMap<K, Queue<EntryEvent<K, V>>>(); private Map<EventType, List<Listener<K,V>>> asyncListenerByType; { asyncListenerByType = new HashMap<EventType, List<Listener<K, V>>>(); for (EventType t : EventType.values()) { asyncListenerByType.put(t, new CopyOnWriteArrayList<Listener<K, V>>()); } } public AsyncDispatcher(final Executor _executor) { executor = _executor; } void addAsyncListener(Listener<K,V> l) { asyncListenerByType.get(l.getEventType()).add(l); } boolean removeAsyncListener(CacheEntryListenerConfiguration<K,V> cfg) { boolean _found = false; for (EventType t : EventType.values()) { _found |= EventHandling.removeCfgMatch(cfg, asyncListenerByType.get(t)); } return _found; } void collectListeners(Collection<Listener<K, V>> l) { for (EventType t : EventType.values()) { l.addAll(asyncListenerByType.get(t)); } } /** * If listeners are registered for this event type, run the listeners or * queue the event, if already something is happening for this key. */ void deliverAsyncEvent(final EntryEvent<K,V> _event) { if (asyncListenerByType.get(_event.getEventType()).isEmpty()) { return; } List<Listener<K,V>> _listeners = new ArrayList<Listener<K, V>>(asyncListenerByType.get(_event.getEventType())); if (_listeners.isEmpty()) { return; } K key = _event.getKey(); synchronized (getLockObject(key)) { Queue<EntryEvent<K,V>> q = keyQueue.get(key); if (q != null) { q.add(_event); return; } q = new LinkedList<EntryEvent<K, V>>(); keyQueue.put(key, q); } runAllListenersInParallel(_event, _listeners); } /** * Pass on runnables to the executor for all listeners. After each event is handled * within the listener we check whether the event is processed by all listeners, by * decrementing a countdown. In case the event is processed completely, we check whether * more is queued up for this key meanwhile. */ void runAllListenersInParallel(final EntryEvent<K, V> _event, List<Listener<K, V>> _listeners) { final AtomicInteger _countDown = new AtomicInteger(_listeners.size()); for (final Listener<K,V> l : _listeners) { Runnable r = new Runnable() { @Override public void run() { try { l.fire(_event); } catch (Throwable t) { t.printStackTrace(); } int _done = _countDown.decrementAndGet(); if (_done == 0) { runMoreOnKeyQueueOrStop(_event.getKey()); } } }; executor.execute(r); } } /** * Check the event queue for this key and process the next event. If no more events are * present remove the queue. */ void runMoreOnKeyQueueOrStop(K key) { EntryEvent<K,V> _event; synchronized (getLockObject(key)) { Queue<EntryEvent<K,V>> q = keyQueue.get(key); if (q.isEmpty()) { keyQueue.remove(key); return; } _event = q.remove(); } List<Listener<K,V>> _listeners = new ArrayList<Listener<K, V>>(asyncListenerByType.get(_event.getEventType())); if (_listeners.isEmpty()) { runMoreOnKeyQueueOrStop(key); return; } runAllListenersInParallel(_event, _listeners); } }