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 merges old and new value if a value is enqueued and another one already exists
038 * in the queue with the same conflation key.  The backing queue is supplied to the constructor and it determines
039 * whether single or multiple producers and consumers are supported.
040 * <p>
041 * Each producer should acquire its own {@link #appender() appender} from the producing thread, and similarly every
042 * consumer should call {@link #poller()} from the consuming thread.  Note that appender listener and poller listener
043 * must be thread safe if multiple producers or consumers are used, e.g. use
044 * {@link AppenderListener#threadLocalSupplier(Supplier)} and {@link PollerListener#threadLocalSupplier(Supplier)} to
045 * create separate listener instances per producer/consumer thread.
046 *
047 * @param <K> the type of the conflation key
048 * @param <V> the type of elements in the queue
049 */
050public class MergeConflationQueue<K,V> implements ExchangeConflationQueue<K,V> {
051
052    private final Map<K,Entry<K,MarkedValue<V>>> entryMap;
053    private final Queue<Entry<K,MarkedValue<V>>> queue;
054    private final Merger<? super K, V> merger;
055
056    private final ThreadLocal<Appender<K,V>> appender = ThreadLocal.withInitial(MergeQueueAppender::new);
057    private final ThreadLocal<ExchangePoller<K,V>> poller = ThreadLocal.withInitial(MergeQueuePoller::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 MergeConflationQueue(final Map<K,Entry<K,MarkedValue<V>>> entryMap,
064                                 final Supplier<? extends Queue<Object>> queueFactory,
065                                 final Merger<? super K, V> merger,
066                                 final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
067                                 final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
068        this(entryMap, (Queue<Entry<K,MarkedValue<V>>>)(Object)queueFactory.get(), merger, appenderListenerSupplier, pollerListenerSupplier);
069    }
070
071    private MergeConflationQueue(final Map<K,Entry<K,MarkedValue<V>>> entryMap,
072                                 final Queue<Entry<K,MarkedValue<V>>> queue,
073                                 final Merger<? super K, V> merger,
074                                 final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
075                                 final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
076        this.entryMap = Objects.requireNonNull(entryMap);
077        this.queue = Objects.requireNonNull(queue);
078        this.merger = Objects.requireNonNull(merger);
079        this.appenderListenerSupplier = Objects.requireNonNull(appenderListenerSupplier);
080        this.pollerListenerSupplier = Objects.requireNonNull(pollerListenerSupplier);
081    }
082
083    /**
084     * Constructor with queue factory and merger.  A concurrent hash map is used to to recycle entries per conflation
085     * key.
086     *
087     * @param queueFactory the factory to create the backing queue
088     * @param merger the merge strategy to use if conflation occurs
089     */
090    public MergeConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
091                                final Merger<? super K, V> merger) {
092        this(queueFactory, merger, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
093    }
094
095    /**
096     * Constructor with queue factory and merger.  A concurrent hash map is used to to recycle entries per conflation
097     * key.
098     *
099     * @param queueFactory the factory to create the backing queue
100     * @param merger the merge strategy to use if conflation occurs
101     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
102     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
103     */
104    public MergeConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
105                                final Merger<? super K, V> merger,
106                                final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
107                                final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
108        this(new ConcurrentHashMap<>(), queueFactory, merger, appenderListenerSupplier, pollerListenerSupplier);
109    }
110
111    /**
112     * Constructor with queue factory, merger and the exhaustive list of conflation keys.  A hash map is pre-initialized
113     * with all the conflation keys and pre-allocated entries.
114     *
115     * @param queueFactory the factory to create the backing queue
116     * @param merger the merge strategy to use if conflation occurs
117     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
118     */
119    public MergeConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
120                                final Merger<? super K, V> merger,
121                                final List<? extends K> allConflationKeys) {
122        this(queueFactory, merger, allConflationKeys, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
123    }
124
125    /**
126     * Constructor with queue factory, merger and the exhaustive list of conflation keys.  A hash map is pre-initialized
127     * with all the conflation keys and pre-allocated entries.
128     *
129     * @param queueFactory the factory to create the backing queue
130     * @param merger the merge strategy to use if conflation occurs
131     * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance
132     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
133     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
134     */
135    public MergeConflationQueue(final Supplier<? extends Queue<Object>> queueFactory,
136                                final Merger<? super K, V> merger,
137                                final List<? extends K> allConflationKeys,
138                                final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
139                                final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
140        this(Entry.eagerlyInitialiseEntryMap(allConflationKeys, MarkedValue::new), queueFactory, merger, appenderListenerSupplier, pollerListenerSupplier);
141    }
142
143    /**
144     * Static constructor method for a conflation queue with queue factory, merger and the conflation key enum class.
145     * An enum map is pre-initialized with all the conflation keys and pre-allocated entries.
146     *
147     * @param queueFactory the factory to create the backing queue
148     * @param merger the merge strategy to use if conflation occurs
149     * @param conflationKeyClass the conflation key enum class
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> MergeConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
155                                                                                       final Merger<? super K, V> merger,
156                                                                                       final Class<K> conflationKeyClass) {
157        return forEnumConflationKey(queueFactory, merger, conflationKeyClass, () -> AppenderListener.NOOP, () -> PollerListener.NOOP);
158    }
159
160    /**
161     * Static constructor method for a conflation queue with queue factory, merger and the conflation key enum class.
162     * An enum map is pre-initialized with all the conflation keys and pre-allocated entries.
163     *
164     * @param queueFactory the factory to create the backing queue
165     * @param merger the merge strategy to use if conflation occurs
166     * @param conflationKeyClass the conflation key enum class
167     * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations
168     * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations
169     * @param <K> the type of the conflation key
170     * @param <V> the type of elements in the queue
171     * @return the new conflation queue instance
172     */
173    public static <K extends Enum<K>,V> MergeConflationQueue<K,V> forEnumConflationKey(final Supplier<? extends Queue<Object>> queueFactory,
174                                                                                       final Merger<? super K, V> merger,
175                                                                                       final Class<K> conflationKeyClass,
176                                                                                       final Supplier<? extends AppenderListener<? super K, ? super V>> appenderListenerSupplier,
177                                                                                       final Supplier<? extends PollerListener<? super K, ? super V>> pollerListenerSupplier) {
178        return new MergeConflationQueue<>(
179                Entry.eagerlyInitialiseEntryEnumMap(conflationKeyClass, MarkedValue::new), queueFactory, merger,
180                appenderListenerSupplier, pollerListenerSupplier
181        );
182    }
183
184    @Override
185    public Appender<K, V> appender() {
186        return appender.get();
187    }
188
189    @Override
190    public ExchangePoller<K, V> poller() {
191        return poller.get();
192    }
193
194    @Override
195    public int size() {
196        return queue.size();
197    }
198
199    @Contended
200    private final static class MarkedValue<V> {
201        enum State {UNCONFIRMED, CONFIRMED, UNUSED}
202        V value;
203        volatile State state = State.UNUSED;
204        MarkedValue<V> initializeWithUnconfirmed(final V value) {
205            this.value = Objects.requireNonNull(value);
206            this.state = State.UNCONFIRMED;
207            return this;
208        }
209        MarkedValue<V> initalizeWithUnused(final V value) {
210            this.value = value;//nulls allowed here
211            this.state = State.UNUSED;
212            return this;
213        }
214        void confirm() {
215            this.state = State.CONFIRMED;
216        }
217
218        void confirmWith(final V value) {
219            this.value = value;
220            this.state = State.CONFIRMED;
221        }
222
223        V markUnusedAndRelease() {
224            awaitFinalState();
225            final V released = value;
226            state = State.UNUSED;
227            this.value = null;
228            return released;
229        }
230
231        boolean isUnused() {
232            return awaitFinalState() == State.UNUSED;
233        }
234
235        private State awaitFinalState() {
236            State s;
237            do {
238                s = state;
239            } while (s == State.UNCONFIRMED);
240            return s;
241        }
242    }
243
244    private final class MergeQueueAppender implements Appender<K,V> {
245        final AppenderListener<? super K, ? super V> appenderListener = appenderListenerSupplier.get();
246        @Contended
247        MarkedValue<V> markedValue = new MarkedValue<>();
248        @Override
249        public V enqueue(final K conflationKey, final V value) {
250            Objects.requireNonNull(value);
251            final Entry<K,MarkedValue<V>> entry = entryMap.computeIfAbsent(conflationKey, k -> new Entry<>(k, new MarkedValue<>()));
252            final MarkedValue<V> newValue = markedValue.initializeWithUnconfirmed(value);
253            final MarkedValue<V> oldValue = entry.value.getAndSet(newValue);
254            final V add;
255            final V old;
256            final AppenderListener.Conflation conflation;
257            try {
258                if (oldValue.isUnused()) {
259                    newValue.confirm();
260                    queue.add(entry);
261                    add = value;
262                    conflation = AppenderListener.Conflation.UNCONFLATED;
263                } else {
264                    final V previous = oldValue.value;
265                    try {
266                        add = merger.merge(conflationKey, previous, value);
267                        newValue.confirmWith(add);
268                        conflation = AppenderListener.Conflation.MERGED;
269                    } catch (final Throwable t) {
270                        newValue.confirmWith(previous);
271                        throw t;
272                    }
273                }
274            } finally {
275                markedValue = oldValue;
276                old = oldValue.markUnusedAndRelease();
277            }
278            //NOTE: ok if below listener throws exception now as it cannot messs with the queue's state
279            appenderListener.enqueued(MergeConflationQueue.this, conflationKey, add, old, conflation);
280            return old;
281        }
282    }
283
284    private final class MergeQueuePoller implements ExchangePoller<K,V> {
285        final PollerListener<? super K, ? super V> pollerListener = pollerListenerSupplier.get();
286        @Contended
287        MarkedValue<V> markedValue = new MarkedValue<>();
288        @Override
289        public V poll(final BiConsumer<? super K, ? super V> consumer, final V exchange) {
290            final Entry<K,MarkedValue<V>> entry = queue.poll();
291            if (entry != null) {
292                final MarkedValue<V> exchangeValue = markedValue.initalizeWithUnused(exchange);
293                final MarkedValue<V> polledValue = entry.value.getAndSet(exchangeValue);
294                final V value = polledValue.markUnusedAndRelease();
295                markedValue = polledValue;
296                consumer.accept(entry.key, value);
297                pollerListener.polled(MergeConflationQueue.this, entry.key, value);
298                return value;
299            } else {
300                pollerListener.polledButFoundEmpty(MergeConflationQueue.this);
301                return null;
302            }
303        }
304    }
305}