All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tools4j.nobark.queue.MergeConflationQueue Maven / Gradle / Ivy

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 nobark (tools4j), Marco Terzer, Anton Anufriev
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.tools4j.nobark.queue;

import sun.misc.Contended;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * A conflation queue implementation that merges old and new value if a value is enqueued and another one already exists
 * in the queue with the same conflation key.  The backing queue is supplied to the constructor and it determines
 * whether single or multiple producers and consumers are supported.
 * 

* Each producer should acquire its own {@link #appender() appender} from the producing thread, and similarly every * consumer should call {@link #poller()} from the consuming thread. Note that appender listener and poller listener * must be thread safe if multiple producers or consumers are used, e.g. use * {@link AppenderListener#threadLocalSupplier(Supplier)} and {@link PollerListener#threadLocalSupplier(Supplier)} to * create separate listener instances per producer/consumer thread. * * @param the type of the conflation key * @param the type of elements in the queue */ public class MergeConflationQueue implements ExchangeConflationQueue { private final Queue>> queue; private final Map>> entryMap; private final Merger merger; private final ThreadLocal> appender = ThreadLocal.withInitial(MergeQueueAppender::new); private final ThreadLocal> poller = ThreadLocal.withInitial(MergeQueuePoller::new); private final Supplier> appenderListenerSupplier; private final Supplier> pollerListenerSupplier; private MergeConflationQueue(final Queue>> queue, final Map>> entryMap, final Merger merger, final Supplier> appenderListenerSupplier, final Supplier> pollerListenerSupplier) { this.queue = Objects.requireNonNull(queue); this.entryMap = Objects.requireNonNull(entryMap); this.merger = Objects.requireNonNull(merger); this.appenderListenerSupplier = Objects.requireNonNull(appenderListenerSupplier); this.pollerListenerSupplier = Objects.requireNonNull(pollerListenerSupplier); } /** * Constructor with queue factory and merger. A concurrent hash map is used to manage entries per conflation * key. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs */ public MergeConflationQueue(final Supplier> queueFactory, final Merger merger) { this(queueFactory, merger, () -> AppenderListener.NOOP, () -> PollerListener.NOOP); } /** * Constructor with queue factory and merger. A concurrent hash map is used to manage entries per conflation * key. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations */ public MergeConflationQueue(final Supplier> queueFactory, final Merger merger, final Supplier> appenderListenerSupplier, final Supplier> pollerListenerSupplier) { this(queueFactory, ConcurrentHashMap::new, merger, appenderListenerSupplier, pollerListenerSupplier); } /** * Constructor with queue factory, entry map factory and merger. * * @param queueFactory the factory to create the backing queue * @param entryMapFactory the factory to create the map that manages entries per conflation key * @param merger the merge strategy to use if conflation occurs * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations */ public MergeConflationQueue(final Supplier> queueFactory, final Supplier> entryMapFactory, final Merger merger, final Supplier> appenderListenerSupplier, final Supplier> pollerListenerSupplier) { this(Factories.createQueue(queueFactory), Factories.createMap(entryMapFactory), merger, appenderListenerSupplier, pollerListenerSupplier); } /** * Constructor with queue factory, merger and the exhaustive list of conflation keys. A hash map is pre-initialized * with all the conflation keys and pre-allocated entries. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance */ public MergeConflationQueue(final Supplier> queueFactory, final Merger merger, final List allConflationKeys) { this(queueFactory, merger, allConflationKeys, () -> AppenderListener.NOOP, () -> PollerListener.NOOP); } /** * Constructor with queue factory, merger and the exhaustive list of conflation keys. A hash map is pre-initialized * with all the conflation keys and pre-allocated entries. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs * @param allConflationKeys all conflation keys that will ever be used with this conflation queue instance * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations */ public MergeConflationQueue(final Supplier> queueFactory, final Merger merger, final List allConflationKeys, final Supplier> appenderListenerSupplier, final Supplier> pollerListenerSupplier) { this(Factories.createQueue(queueFactory), Entry.eagerlyInitialiseEntryMap(allConflationKeys, MarkedValue::new), merger, appenderListenerSupplier, pollerListenerSupplier); } /** * Static constructor method for a conflation queue with queue factory, merger and the conflation key enum class. * An enum map is pre-initialized with all the conflation keys and pre-allocated entries. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs * @param conflationKeyClass the conflation key enum class * @param the type of the conflation key * @param the type of elements in the queue * @return the new conflation queue instance */ public static ,V> MergeConflationQueue forEnumConflationKey(final Supplier> queueFactory, final Merger merger, final Class conflationKeyClass) { return forEnumConflationKey(queueFactory, merger, conflationKeyClass, () -> AppenderListener.NOOP, () -> PollerListener.NOOP); } /** * Static constructor method for a conflation queue with queue factory, merger and the conflation key enum class. * An enum map is pre-initialized with all the conflation keys and pre-allocated entries. * * @param queueFactory the factory to create the backing queue * @param merger the merge strategy to use if conflation occurs * @param conflationKeyClass the conflation key enum class * @param appenderListenerSupplier a supplier for a listener to monitor the enqueue operations * @param pollerListenerSupplier a supplier for a listener to monitor the poll operations * @param the type of the conflation key * @param the type of elements in the queue * @return the new conflation queue instance */ public static ,V> MergeConflationQueue forEnumConflationKey(final Supplier> queueFactory, final Merger merger, final Class conflationKeyClass, final Supplier> appenderListenerSupplier, final Supplier> pollerListenerSupplier) { return new MergeConflationQueue<>( Factories.createQueue(queueFactory), Entry.eagerlyInitialiseEntryEnumMap(conflationKeyClass, MarkedValue::new), merger, appenderListenerSupplier, pollerListenerSupplier ); } @Override public Appender appender() { return appender.get(); } @Override public ExchangePoller poller() { return poller.get(); } @Override public int size() { return queue.size(); } @Contended private final static class MarkedValue { enum State {UNCONFIRMED, CONFIRMED, UNUSED} V value; volatile State state = State.UNUSED; MarkedValue initializeWithUnconfirmed(final V value) { this.value = Objects.requireNonNull(value); this.state = State.UNCONFIRMED; return this; } MarkedValue initalizeWithUnused(final V value) { this.value = value;//nulls allowed here this.state = State.UNUSED; return this; } void confirm() { this.state = State.CONFIRMED; } void confirmWith(final V value) { this.value = value; this.state = State.CONFIRMED; } V awaitAndRelease() { awaitFinalState(); return release(); } V release() { final V released = value; state = State.UNUSED; this.value = null; return released; } boolean isUnused() { return awaitFinalState() == State.UNUSED; } private State awaitFinalState() { State s; do { s = state; } while (s == State.UNCONFIRMED); return s; } } private final class MergeQueueAppender implements Appender { final AppenderListener appenderListener = appenderListenerSupplier.get(); @Contended MarkedValue markedValue = new MarkedValue<>(); @Override public V enqueue(final K conflationKey, final V value) { Objects.requireNonNull(value); final Entry> entry = entryMap.computeIfAbsent(conflationKey, k -> new Entry<>(k, new MarkedValue<>())); final MarkedValue newValue = markedValue.initializeWithUnconfirmed(value); final MarkedValue oldValue = entry.value.getAndSet(newValue); final V add; final V old; final AppenderListener.Conflation conflation; try { if (oldValue.isUnused()) { old = oldValue.release(); add = value; newValue.confirm(); queue.add(entry); conflation = AppenderListener.Conflation.UNCONFLATED; } else { old = oldValue.awaitAndRelease(); try { add = merger.merge(conflationKey, old, value); } catch (final Throwable t) { newValue.confirmWith(old); throw t; } newValue.confirmWith(add); conflation = AppenderListener.Conflation.MERGED; } } finally { markedValue = oldValue; } //NOTE: ok if below listener throws exception now as it cannot messs with the queue's state appenderListener.enqueued(MergeConflationQueue.this, conflationKey, add, old, conflation); return old; } } private final class MergeQueuePoller implements ExchangePoller { final PollerListener pollerListener = pollerListenerSupplier.get(); @Contended MarkedValue markedValue = new MarkedValue<>(); @Override public V poll(final BiConsumer consumer, final V exchange) { final Entry> entry = queue.poll(); if (entry != null) { final MarkedValue exchangeValue = markedValue.initalizeWithUnused(exchange); final MarkedValue polledValue = entry.value.getAndSet(exchangeValue); final V value = polledValue.awaitAndRelease(); markedValue = polledValue; consumer.accept(entry.key, value); pollerListener.polled(MergeConflationQueue.this, entry.key, value); return value; } else { pollerListener.polledButFoundEmpty(MergeConflationQueue.this); return null; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy