001/**
002 * The MIT License (MIT)
003 *
004 * Copyright (c) 2019 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    private final Queue<Entry<K,V>> queue;
050    private final Map<K,Entry<K,V>> entryMap;
051
052    private final Appender<K,V> appender = new AtomicQueueAppender();
053    private final Poller<K,V> poller = new AtomicQueuePoller();
054
055    private final AppenderListener<? super K, ? super V> appenderListener;
056    private final PollerListener<? super K, ? super V> pollerListener;
057
058    private AtomicConflationQueue(final Queue<Entry<K,V>> queue,
059                                  final Map<K,Entry<K,V>> entryMap,
060                                  final AppenderListener<? super K, ? super V> appenderListener,
061                                  final PollerListener<? super K, ? super V> pollerListener) {
062        this.queue = Objects.requireNonNull(queue);
063        this.entryMap = Objects.requireNonNull(entryMap);
064        this.appenderListener= Objects.requireNonNull(appenderListener);
065        this.pollerListener = Objects.requireNonNull(pollerListener);
066    }
067
068    /**
069     * Constructor with queue factory.  A concurrent hash map is used to manage entries per conflation key.
070     *
071     * @param queueFactory the factory to create the backing queue
072     */
073    public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory) {
074        this(queueFactory, AppenderListener.NOOP, PollerListener.NOOP);
075    }
076
077    /**
078     * Constructor with queue factory.  A concurrent hash map is used to manage entries per conflation key.
079     *
080     * @param queueFactory the factory to create the backing queue
081     * @param appenderListener a listener to monitor the enqueue operations
082     * @param pollerListener a listener to monitor the poll operations
083     */
084    public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
085                                 final AppenderListener<? super K, ? super V> appenderListener,
086                                 final PollerListener<? super K, ? super V> pollerListener) {
087        this(queueFactory, ConcurrentHashMap::new, appenderListener, pollerListener);
088    }
089
090    /**
091     * Constructor with queue factory and entry map factory.
092     *
093     * @param queueFactory      the factory to create the backing queue
094     * @param entryMapFactory   the factory to create the map that manages entries per conflation key
095     * @param appenderListener  a listener to monitor the enqueue operations
096     * @param pollerListener    a listener to monitor the poll operations
097     */
098    public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
099                                 final Supplier<? extends Map<Object, Object>> entryMapFactory,
100                                 final AppenderListener<? super K, ? super V> appenderListener,
101                                 final PollerListener<? super K, ? super V> pollerListener) {
102        this(Factories.createQueue(queueFactory), Factories.createMap(entryMapFactory), appenderListener, pollerListener);
103    }
104
105    /**
106     * Constructor with queue factory and the exhaustive list of conflation keys.  A hash map is pre-initialized with
107     * all the conflation keys and pre-allocated entries.
108     *
109     * @param queueFactory the factory to create the backing queue
110     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
111     */
112    public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
113                                 final List<? extends K> allConflationKeys) {
114        this(queueFactory, allConflationKeys, AppenderListener.NOOP, PollerListener.NOOP);
115    }
116
117    /**
118     * Constructor with queue factory and the exhaustive list of conflation keys.  A hash map is pre-initialized with
119     * all the conflation keys and pre-allocated entries.
120     *
121     * @param queueFactory the factory to create the backing queue
122     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
123     * @param appenderListener a listener to monitor the enqueue operations
124     * @param pollerListener a listener to monitor the poll operations
125     */
126    public AtomicConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
127                                 final List<? extends K> allConflationKeys,
128                                 final AppenderListener<? super K, ? super V> appenderListener,
129                                 final PollerListener<? super K, ? super V> pollerListener) {
130        this(Factories.createQueue(queueFactory), Entry.eagerlyInitialiseEntryMap(allConflationKeys, () -> null),
131                appenderListener, pollerListener);
132    }
133
134    /**
135     * Static constructor method for a conflation queue with queue factory and the conflation key enum class.  An enum
136     * map is pre-initialized with all the conflation keys and pre-allocated entries.
137     *
138     * @param queueFactory the factory to create the backing queue
139     * @param conflationKeyClass the conflation key enum class
140     * @param <K> the type of the conflation key
141     * @param <V> the type of elements in the queue
142     * @return the new conflation queue instance
143     */
144    public static <K extends Enum<K>,V> AtomicConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
145                                                                                        final Class<K> conflationKeyClass) {
146        return forEnumConflationKey(queueFactory, conflationKeyClass, AppenderListener.NOOP, PollerListener.NOOP);
147    }
148
149    /**
150     * Static constructor method for a conflation queue with queue factory and the conflation key enum class.  An enum
151     * map is pre-initialized with all the conflation keys and pre-allocated entries.
152     *
153     * @param queueFactory the factory to create the backing queue
154     * @param conflationKeyClass the conflation key enum class
155     * @param appenderListener a listener to monitor the enqueue operations
156     * @param pollerListener a listener to monitor the poll operations
157     * @param <K> the type of the conflation key
158     * @param <V> the type of elements in the queue
159     * @return the new conflation queue instance
160     */
161    public static <K extends Enum<K>,V> AtomicConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
162                                                                                        final Class<K> conflationKeyClass,
163                                                                                        final AppenderListener<? super K, ? super V> appenderListener,
164                                                                                        final PollerListener<? super K, ? super V> pollerListener) {
165        return new AtomicConflationQueue<>(Factories.createQueue(queueFactory),
166                Entry.eagerlyInitialiseEntryEnumMap(conflationKeyClass, () -> null), appenderListener, pollerListener);
167    }
168
169    @Override
170    public Appender<K, V> appender() {
171        return appender;
172    }
173
174    @Override
175    public Poller<K, V> poller() {
176        return poller;
177    }
178
179    @Override
180    public int size() {
181        return queue.size();
182    }
183
184    private final class AtomicQueueAppender implements Appender<K,V> {
185        @Override
186        public V enqueue(final K conflationKey, final V value) {
187            Objects.requireNonNull(value);
188            final Entry<K,V> entry = entryMap.computeIfAbsent(conflationKey, k -> new Entry<>(k, null));
189            final V old = entry.value.getAndSet(value);
190            final AppenderListener.Conflation conflation;
191            if (old == null) {
192                queue.add(entry);
193                conflation = AppenderListener.Conflation.UNCONFLATED;
194            } else {
195                conflation = AppenderListener.Conflation.EVICTED;
196            }
197            appenderListener.enqueued(AtomicConflationQueue.this, conflationKey, value, old, conflation);
198            return old;
199        }
200    }
201
202    private final class AtomicQueuePoller implements Poller<K,V> {
203        @Override
204        public V poll(final BiConsumer<? super K, ? super V> consumer) {
205            final Entry<K,V> entry = queue.poll();
206            if (entry != null) {
207                final V value = entry.value.getAndSet(null);
208                consumer.accept(entry.key, value);
209                pollerListener.polled(AtomicConflationQueue.this, entry.key, value);
210                return value;
211            } else {
212                pollerListener.polledButFoundEmpty(AtomicConflationQueue.this);
213                return null;
214            }
215        }
216    }
217}