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 sun.misc.Contended;
027
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.Queue;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.function.BiConsumer;
034import java.util.function.Supplier;
035
036/**
037 * A conflation queue implementation that evicts old values from the queue if a new one is enqueued with the same
038 * conflation key.  The {@link AtomicConflationQueue} is very similar but this queue implements
039 * {@link ExchangeConflationQueue} and hence supports value exchange between producer and consumer.  The backing queue
040 * is supplied to the constructor and it determines whether single or multiple producers and consumers are supported.
041 * <p>
042 * Each producer should acquire its own {@link #appender() appender} from the producing thread, and similarly every
043 * consumer should call {@link #poller()} from the consuming thread.  Note that appender listener and poller listener
044 * must be thread safe if multiple producers or consumers are used, e.g. use
045 * {@link AppenderListener#threadLocalSupplier(Supplier)} and {@link PollerListener#threadLocalSupplier(Supplier)} to
046 * create separate listener instances per producer/consumer thread.
047 *
048 * @param <K> the type of the conflation key
049 * @param <V> the type of elements in the queue
050 */
051public class EvictConflationQueue<K,V> implements ExchangeConflationQueue<K,V> {
052
053    private final Map<K,Entry<K,MarkedValue<V>>> entryMap;
054    private final Queue<Entry<K,MarkedValue<V>>> queue;
055
056    private final ThreadLocal<Appender<K,V>> appender = ThreadLocal.withInitial(EvictQueueAppender::new);
057    private final ThreadLocal<ExchangePoller<K,V>> poller = ThreadLocal.withInitial(EvictQueuePoller::new);
058
059    private final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier;
060    private final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier;
061
062    @SuppressWarnings("unchecked")//casting a queue that takes objects to one that takes Entry is fine as long as we only add Entry objects
063    private EvictConflationQueue(final Map<K,Entry<K,MarkedValue<V>>> entryMap,
064                                 final Supplier<? extends Queue<Object>> queueFactory,
065                                 final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
066                                 final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
067        this(entryMap, (Queue<Entry<K,MarkedValue<V>>>)(Object)queueFactory.get(), appenderListenerSupplier, pollerListenerSupplier);
068    }
069
070    private EvictConflationQueue(final Map<K,Entry<K,MarkedValue<V>>> entryMap,
071                                 final Queue<Entry<K,MarkedValue<V>>> queue,
072                                 final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
073                                 final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
074        this.entryMap = Objects.requireNonNull(entryMap);
075        this.queue = Objects.requireNonNull(queue);
076        this.appenderListenerSupplier = Objects.requireNonNull(appenderListenerSupplier);
077        this.pollerListenerSupplier = Objects.requireNonNull(pollerListenerSupplier);
078    }
079
080    /**
081     * Constructor with queue factory.  A concurrent hash map is used to to recycle entries per conflation key.
082     *
083     * @param queueFactory the factory to create the backing queue
084     */
085    public EvictConflationQueue(final Supplier<? extends Queue<Object>> queueFactory) {
086        this(queueFactory, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
087    }
088
089    /**
090     * Constructor with queue factory.  A concurrent hash map is used to to recycle entries per conflation key.
091     *
092     * @param queueFactory the factory to create the backing queue
093     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
094     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
095     */
096    public EvictConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
097                                final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
098                                final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
099        this(new ConcurrentHashMap<>(), queueFactory, appenderListenerSupplier, pollerListenerSupplier);
100    }
101
102    /**
103     * Constructor with queue factory and the exhaustive list of conflation keys.  A hash map is pre-initialized with
104     * all the conflation keys and pre-allocated entries.
105     *
106     * @param queueFactory the factory to create the backing queue
107     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
108     */
109    public EvictConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
110                                final List<? extends K> allConflationKeys) {
111        this(queueFactory, allConflationKeys, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
112    }
113
114    /**
115     * Constructor with queue factory and the exhaustive list of conflation keys.  A hash map is pre-initialized with
116     * all the conflation keys and pre-allocated entries.
117     *
118     * @param queueFactory the factory to create the backing queue
119     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
120     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
121     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
122     */
123    public EvictConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
124                                final List<? extends K> allConflationKeys,
125                                final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
126                                final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
127        this(Entry.eagerlyInitialiseEntryMap(allConflationKeys, MarkedValue::new), queueFactory, appenderListenerSupplier, pollerListenerSupplier);
128    }
129
130    /**
131     * Static constructor method for a conflation queue with queue factory and the conflation key enum class.  An enum
132     * map is pre-initialized with all the conflation keys and pre-allocated entries.
133     *
134     * @param queueFactory the factory to create the backing queue
135     * @param conflationKeyClass the conflation key enum class
136     * @param <K> the type of the conflation key
137     * @param <V> the type of elements in the queue
138     * @return the new conflation queue instance
139     */
140    public static <K extends Enum<K>,V> EvictConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
141                                                                                       final Class<K> conflationKeyClass) {
142        return forEnumConflationKey(queueFactory, conflationKeyClass, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
143    }
144
145    /**
146     * Static constructor method for a conflation queue with queue factory and the conflation key enum class.  An enum
147     * map is pre-initialized with all the conflation keys and pre-allocated entries.
148     *
149     * @param queueFactory the factory to create the backing queue
150     * @param conflationKeyClass the conflation key enum class
151     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
152     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
153     * @param <K> the type of the conflation key
154     * @param <V> the type of elements in the queue
155     * @return the new conflation queue instance
156     */
157    public static <K extends Enum<K>,V> EvictConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
158                                                                                       final Class<K> conflationKeyClass,
159                                                                                       final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
160                                                                                       final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
161        return new EvictConflationQueue<>(
162                Entry.eagerlyInitialiseEntryEnumMap(conflationKeyClass, MarkedValue::new), queueFactory,
163                appenderListenerSupplier, pollerListenerSupplier
164        );
165    }
166
167    @Override
168    public Appender<K, V> appender() {
169        return appender.get();
170    }
171
172    @Override
173    public ExchangePoller<K, V> poller() {
174        return poller.get();
175    }
176
177    @Override
178    public int size() {
179        return queue.size();
180    }
181
182    @Contended
183    private final static class MarkedValue<V> {
184        enum State {USED, UNUSED}
185        V value;
186        State state = State.UNUSED;
187        MarkedValue<V> initializeWithUsed(final V value) {
188            this.value = Objects.requireNonNull(value);
189            this.state = State.USED;
190            return this;
191        }
192        MarkedValue<V> initalizeWithUnused(final V value) {
193            this.value = value;//nulls allowed here
194            this.state = State.UNUSED;
195            return this;
196        }
197
198        V markUnusedAndRelease() {
199            final V released = value;
200            state = State.UNUSED;
201            value = null;
202            return released;
203        }
204
205        boolean isUnused() {
206            return state == State.UNUSED;
207        }
208    }
209
210    private final class EvictQueueAppender implements Appender<K,V> {
211        final AppenderListener<? super K, ? super V> appenderListener = appenderListenerSupplier.get();
212        @Contended
213        MarkedValue<V> markedValue = new MarkedValue<>();
214        @Override
215        public V enqueue(final K conflationKey, final V value) {
216            Objects.requireNonNull(value);
217            final Entry<K, MarkedValue<V>> entry = entryMap.computeIfAbsent(conflationKey, k -> new Entry<>(k, new MarkedValue<>()));
218            final MarkedValue<V> newValue = markedValue.initializeWithUsed(value);
219            final MarkedValue<V> oldValue = entry.value.getAndSet(newValue);
220            final AppenderListener.Conflation conflation;
221            if (oldValue.isUnused()) {
222                queue.add(entry);
223                conflation = AppenderListener.Conflation.UNCONFLATED;
224            } else {
225                conflation = AppenderListener.Conflation.EVICTED;
226            }
227            markedValue = oldValue;
228            final V old = oldValue.markUnusedAndRelease();
229            //NOTE: ok if listener throws exception now as it cannot mess up this queue's state
230            appenderListener.enqueued(EvictConflationQueue.this, conflationKey, value, old, conflation);
231            return old;
232        }
233    }
234
235    private final class EvictQueuePoller implements ExchangePoller<K,V> {
236        final PollerListener<? super K, ? super V> pollerListener = pollerListenerSupplier.get();
237        @Contended
238        MarkedValue<V> markedValue = new MarkedValue<>();
239        @Override
240        public V poll(final BiConsumer<? super K, ? super V> consumer, final V exchange) {
241            final Entry<K,MarkedValue<V>> entry = queue.poll();
242            if (entry != null) {
243                final MarkedValue<V> exchangeValue = markedValue.initalizeWithUnused(exchange);
244                final MarkedValue<V> polledValue = entry.value.getAndSet(exchangeValue);
245                final V value = polledValue.markUnusedAndRelease();
246                markedValue = polledValue;
247                consumer.accept(entry.key, value);
248                pollerListener.polled(EvictConflationQueue.this, entry.key, value);
249                return value;
250            } else {
251                pollerListener.polledButFoundEmpty(EvictConflationQueue.this);
252                return null;
253            }
254        }
255    }
256}