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}