001/** 002 * The MIT License (MIT) 003 * 004 * Copyright (c) 2018 nobark (tools4j), Marco Terzer, Anton Anufriev 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024package org.tools4j.nobark.queue; 025 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Queue; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.function.BiConsumer; 032import java.util.function.Supplier; 033 034/** 035 * A conflation queue implementation that atomically evicts old values from the queue if a new one is enqueued with the 036 * same conflation key. The {@link EvictConflationQueue} is very similar but not atomic and it supports value exchange 037 * on polling, which this queue does not. A backing queue is supplied to the constructor and it determines whether 038 * single or multiple producers and consumers are supported. 039 * <p> 040 * {@link #appender() Appender} and {@link #poller()} are both stateless and hence thread-safe. Note that appender 041 * listener and poller listener must also be thread safe if multiple producers or consumers are used, e.g. use 042 * {@link AppenderListener#threadLocal(Supplier)} and {@link PollerListener#threadLocal(Supplier)} to create thread 043 * local listener instances. 044 * 045 * @param <K> the type of the conflation key 046 * @param <V> the type of elements in the queue 047 */ 048public class AtomicConflationQueue<K,V> implements ConflationQueue<K,V> { 049 050 private final Map<K,Entry<K,V>> entryMap; 051 private final Queue<Entry<K,V>> queue; 052 053 private final Appender<K,V> appender = new AtomicQueueAppender(); 054 private final Poller<K,V> poller = new AtomicQueuePoller(); 055 056 private final AppenderListener<? super K, ? super V> appenderListener; 057 private final PollerListener<? super K, ? super V> pollerListener; 058 059 @SuppressWarnings("unchecked")//casting a queue that takes objects to one that takes Entry is fine as long as we only add Entry objects 060 private AtomicConflationQueue(final Map<K,Entry<K,V>> entryMap, 061 final Supplier<? extends Queue<Object>> queueFactory, 062 final AppenderListener<? super K, ? super V> appenderListener, 063 final PollerListener<? super K, ? super V> pollerListener) { 064 this(entryMap, (Queue<Entry<K,V>>)(Object)queueFactory.get(), appenderListener, pollerListener); 065 } 066 067 private AtomicConflationQueue(final Map<K,Entry<K,V>> entryMap, 068 final Queue<Entry<K,V>> queue, 069 final AppenderListener<? super K, ? super V> appenderListener, 070 final PollerListener<? super K, ? super V> pollerListener) { 071 this.entryMap = Objects.requireNonNull(entryMap); 072 this.queue = Objects.requireNonNull(queue); 073 this.appenderListener= Objects.requireNonNull(appenderListener); 074 this.pollerListener = Objects.requireNonNull(pollerListener); 075 } 076 077 /** 078 * Constructor with queue factory. A concurrent hash map is used to to recycle entries per conflation key. 079 * 080 * @param queueFactory the factory to create the backing queue 081 */ 082 public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory) { 083 this(queueFactory, AppenderListener.NOOP, PollerListener.NOOP); 084 } 085 086 /** 087 * Constructor with queue factory. A concurrent hash map is used to to recycle entries per conflation key. 088 * 089 * @param queueFactory the factory to create the backing queue 090 * @param appenderListener a listener to monitor the enqueue operations 091 * @param pollerListener a listener to monitor the poll operations 092 */ 093 public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory, 094 final AppenderListener<? super K, ? super V> appenderListener, 095 final PollerListener<? super K, ? super V> pollerListener) { 096 this(new ConcurrentHashMap<>(), queueFactory, appenderListener, pollerListener); 097 } 098 099 /** 100 * Constructor with queue factory and the exhaustive list of conflation keys. A hash map is pre-initialized with 101 * all the conflation keys and pre-allocated entries. 102 * 103 * @param queueFactory the factory to create the backing queue 104 * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance 105 */ 106 public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory, 107 final List<? extends K> allConflationKeys) { 108 this(queueFactory, allConflationKeys, AppenderListener.NOOP, PollerListener.NOOP); 109 } 110 111 /** 112 * Constructor with queue factory and the exhaustive list of conflation keys. A hash map is pre-initialized with 113 * all the conflation keys and pre-allocated entries. 114 * 115 * @param queueFactory the factory to create the backing queue 116 * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance 117 * @param appenderListener a listener to monitor the enqueue operations 118 * @param pollerListener a listener to monitor the poll operations 119 */ 120 public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory, 121 final List<? extends K> allConflationKeys, 122 final AppenderListener<? super K, ? super V> appenderListener, 123 final PollerListener<? super K, ? super V> pollerListener) { 124 this(Entry.eagerlyInitialiseEntryMap(allConflationKeys, () -> null), queueFactory, appenderListener, pollerListener); 125 } 126 127 /** 128 * Static constructor method for a conflation queue with queue factory and the conflation key enum class. An enum 129 * map is pre-initialized with all the conflation keys and pre-allocated entries. 130 * 131 * @param queueFactory the factory to create the backing queue 132 * @param conflationKeyClass the conflation key enum class 133 * @param <K> the type of the conflation key 134 * @param <V> the type of elements in the queue 135 * @return the new conflation queue instance 136 */ 137 public static <K extends Enum<K>,V> AtomicConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory, 138 final Class<K> conflationKeyClass) { 139 return forEnumConflationKey(queueFactory, conflationKeyClass, AppenderListener.NOOP, PollerListener.NOOP); 140 } 141 142 /** 143 * Static constructor method for a conflation queue with queue factory and the conflation key enum class. An enum 144 * map is pre-initialized with all the conflation keys and pre-allocated entries. 145 * 146 * @param queueFactory the factory to create the backing queue 147 * @param conflationKeyClass the conflation key enum class 148 * @param appenderListener a listener to monitor the enqueue operations 149 * @param pollerListener a listener to monitor the poll operations 150 * @param <K> the type of the conflation key 151 * @param <V> the type of elements in the queue 152 * @return the new conflation queue instance 153 */ 154 public static <K extends Enum<K>,V> AtomicConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory, 155 final Class<K> conflationKeyClass, 156 final AppenderListener<? super K, ? super V> appenderListener, 157 final PollerListener<? super K, ? super V> pollerListener) { 158 return new AtomicConflationQueue<>( 159 Entry.eagerlyInitialiseEntryEnumMap(conflationKeyClass, () -> null), queueFactory, 160 appenderListener, pollerListener 161 ); 162 } 163 164 @Override 165 public Appender<K, V> appender() { 166 return appender; 167 } 168 169 @Override 170 public Poller<K, V> poller() { 171 return poller; 172 } 173 174 @Override 175 public int size() { 176 return queue.size(); 177 } 178 179 private final class AtomicQueueAppender implements Appender<K,V> { 180 @Override 181 public V enqueue(final K conflationKey, final V value) { 182 Objects.requireNonNull(value); 183 final Entry<K,V> entry = entryMap.computeIfAbsent(conflationKey, k -> new Entry<>(k, null)); 184 final V old = entry.value.getAndSet(value); 185 final AppenderListener.Conflation conflation; 186 if (old == null) { 187 queue.add(entry); 188 conflation = AppenderListener.Conflation.UNCONFLATED; 189 } else { 190 conflation = AppenderListener.Conflation.EVICTED; 191 } 192 appenderListener.enqueued(AtomicConflationQueue.this, conflationKey, value, old, conflation); 193 return old; 194 } 195 } 196 197 private final class AtomicQueuePoller implements Poller<K,V> { 198 @Override 199 public V poll(final BiConsumer<? super K, ? super V> consumer) { 200 final Entry<K,V> entry = queue.poll(); 201 if (entry != null) { 202 final V value = entry.value.getAndSet(null); 203 consumer.accept(entry.key, value); 204 pollerListener.polled(AtomicConflationQueue.this, entry.key, value); 205 return value; 206 } else { 207 pollerListener.polledButFoundEmpty(AtomicConflationQueue.this); 208 return null; 209 } 210 } 211 } 212}