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

com.crankuptheamps.client.ConflatingRecoveryPointAdapter Maven / Gradle / Ivy

///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2020 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties.  This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights.  This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////

package com.crankuptheamps.client;

import java.beans.ExceptionListener;
import java.lang.Iterable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.BookmarkField;

/**
 * An implementation of the {@link RecoveryPointAdapter} interface that is meant
 * to act as a wrapper around another recovery point adapter instance,
 * delegating periodic updates to its wrapped adapter on an asynchronous
 * update thread (which this instance starts). The intent is to decouple
 * recovery state updates from message discards on a bookmark replay
 * subscriptions so that fast paced subscriptions don't cause a rate of
 * updates that consume too much bandwidth or overwhelm recovery point
 * adapter implementations that use slower storage (slowing message
 * discards).
 */
public class ConflatingRecoveryPointAdapter implements RecoveryPointAdapter
{
    /**
     * The wrapped recovery point adapter instance we delegate to.
     */
    protected RecoveryPointAdapter _adapter = null;

    /**
     * A concurrent hash map of subscription id's (known to this adapter)
     * mapped to Long's that count how many updates have
     * occurred for that subscription.
     */
    protected final ConcurrentHashMap _counts =
            new ConcurrentHashMap();

    /**
     * A concurrent hash map of subscription id's (known to this adapter)
     * mapped to Long's that represent the timestamp of when
     * the subscription's state was last written to the adapter.
     */
    protected final ConcurrentHashMap _timers =
            new ConcurrentHashMap();

    /**
     * A concurrent hash map of subscription id's mapped to the RecoveryPoint
     * received from the most recent update.
     */
    protected final ConcurrentHashMap _latestUpdates =
            new ConcurrentHashMap();

    /**
     * The threshold used to determine if a subscription is due for an update
     * operation, based upon whether the subscription's update count exceeds
     * this threshold since the last update operation.
     */
    protected volatile long _updateThreshold = 10;

    /**
     * The threshold used to determine if a subscription is due for an update
     * operation, based upon whether this many milliseconds have elapsed
     * since the last update operation.
     */
    protected volatile long _timeoutMillis = 2000L;

    /**
     * The threshold for idle time between updates when timers should
     * be checked again.
     */
    protected volatile long _updateIntervalMillis = 2000L;

    /**
     * The background worker thread that persists subscription discard state
     * to the SOW.
     */
    protected UpdateThread _thread = null;

    /**
     * Indicates whether this bookmark store has been closed.
     */
    protected volatile boolean _closed = false;

    /**
     * Indicates whether all updates should be flushed regardless.
     */
    protected volatile boolean _updateAll = false;

    /**
     * The exception listener for this client and both underlying HAClient
     * instances.
     */
    protected volatile ExceptionListener _exceptionListener = null;

    private final Lock lock= new ReentrantLock();
    private final Condition _updatesReady  = lock.newCondition();

    /**
     * Constructs an instance of the recovery point adapter interface that is
     * meant to act as a wrapper around another recovery point adapter instance,
     * delegating periodic updates to its wrapped adapter on an asynchronous
     * update thread (which this instance starts). The intent is to decouple
     * recovery state updates from message discards on a bookmark replay
     * subscriptions so that fast paced subscriptions don't cause a rate of
     * updates that consume too much bandwidth or overwhelm recovery point
     * adapter implementations that use slower storage (slowing message
     * discards).
     *
     * The update threshold is set to default of 10.
     * The update timeout is set to default 2,000 milliseconds.
     * The update interval, at which the update thread will check for updates,
     * is set to the default of 2,000 milliseconds
     * To use any non-default values, use the other constructor.
     *
     * @param adapter The adapter that is sent conflated updates.
     */
    public ConflatingRecoveryPointAdapter(RecoveryPointAdapter adapter) {
        _adapter = adapter;

        // Start the worker to asynchronously handle updates
        _thread = new UpdateThread(
            "ConflatingRecoveryPointAdapter" + CommandId.nextIdentifier());
        _thread.start();
    }

    /**
     * Constructs an instance of the recovery point adapter interface that is
     * meant to act as a wrapper around another recovery point adapter instance,
     * delegating periodic updates to its wrapped adapter on an asynchronous
     * update thread (which this instance starts). The intent is to decouple
     * recovery state updates from message discards on a bookmark replay
     * subscriptions so that fast paced subscriptions don't cause a rate of
     * updates that consume too much bandwidth or overwhelm recovery point
     * adapter implementations that use slower storage (slowing message
     * discards).
     *
     * @param adapter The adapter that is sent conflated updates.
     * @param updateThreshold The maximum number of updates to a subId
     * before a conflated update is delivered.
     * @param timeoutMillis The maximum amount of time in milliseconds between
     * conflated updates for each subId.
     * @param updateIntervalMillis The maximum amount of time in milliseconds
     * the update thread can sit idle before checking for timeouts.
     */
    public ConflatingRecoveryPointAdapter(RecoveryPointAdapter adapter,
                                          long updateThreshold,
                                          long timeoutMillis,
                                          long updateIntervalMillis) {
        _adapter = adapter;
        _updateThreshold = updateThreshold;
        _timeoutMillis = timeoutMillis;
        _updateIntervalMillis = updateIntervalMillis;

        // Start the worker to asynchronously handle updates
        _thread = new UpdateThread(
            "ConflatingRecoveryPointAdapter" + CommandId.nextIdentifier());
        _thread.start();
    }

    /**
     * Sets the {@link java.beans.ExceptionListener} instance used for
     * communicating absorbed exceptions.
     *
     * @param exceptionListener The exception listener instance to invoke for
     *        internal exceptions.
     */
    public void setExceptionListener(ExceptionListener exceptionListener) {
        this._exceptionListener = exceptionListener;
    }

    /**
     * Returns the {@link java.beans.ExceptionListener} instance used for
     * communicating absorbed exceptions.
     *
     * @return The current exception listener.
     */
    public ExceptionListener getExceptionListener() {
        return this._exceptionListener;
    }

    /**
     * Confalte an update to the newest recovery point.
     * @param recoveryPoint The newest recovery point to save.
     */
    public void update(RecoveryPoint recoveryPoint) {
        RecoveryPoint rpCopy = recoveryPoint.copy();
        Field subId = recoveryPoint.getSubId();
        lock.lock();
        try {
            // If it's a repeat subid
            if (_latestUpdates.replace(subId, rpCopy) != null) {
                long count = _counts.get(subId);
                _counts.put(subId, ++count);
                if (count >= _updateThreshold) {
                    _updatesReady.signalAll();
                }
            }
            else { // New sub id
                _latestUpdates.put(subId.copy(), rpCopy);
                _counts.put(subId.copy(), 1L);
                if (_timeoutMillis != 0) {
                    _timers.put(subId.copy(), System.currentTimeMillis());
                }
            }
        }
        finally {
            lock.unlock();
        }
    }

    /**
     * Remove all recovery point information from self and adapter.
     */
    public void purge() throws Exception {
        lock.lock();
        try {
            if (_adapter != null) {
                runUpdateAll();
                _adapter.purge();
            }
            _latestUpdates.clear();
            _counts.clear();
            _timers.clear();
        }
        finally {
            lock.unlock();
        }
    }

    /**
     * Remove all recovery point information for subId from self and adapter.
     *
     * @param subId The subId to remove.
     */
    public void purge(Field subId) throws Exception {
        lock.lock();
        try {
            if (_adapter != null) {
                runUpdateAll();
                _adapter.purge(subId);
            }
            _latestUpdates.remove(subId);
            _counts.remove(subId);
            _timers.remove(subId);
        }
        finally {
            lock.unlock();
        }
    }

    /**
     * Force all updates to the underlying adapter, then close it.
     */
    public void close() throws Exception {
        lock.lock();
        try {
            if (!_closed) {
                runUpdateAll();
                _closed = true;
                _updatesReady.signalAll();
            }
        }
        finally {
            lock.unlock();
        }

        if (_thread != null) {
            _thread.join();
            _thread = null;
        }
        if (_adapter != null) {
            _adapter.close();
        }
    }

    /**
     * Implements {@link Iterator#hasNext}.
     */
    public boolean hasNext()
    {
        return _adapter.hasNext();
    }

    /**
     * Implements {@link Iterator#next} for a message stream.
     */
    public RecoveryPoint next()
    {
        return _adapter.next();
    }

    /**
     * Operation not supported. Implements {@link Iterator#remove} to throw UnsupportedOperationException.
     */
    public void remove()
    {
        throw new UnsupportedOperationException();
    }

    /**
     * Implements {@link Iterable#iterator} to return this instance.
     */
    public Iterator iterator()
    {
        return this;
    }

    /**
     * Force all held updates to be flushed to the underlying adapter.
     */
    public void updateAll() {
        lock.lock();
        try {
            runUpdateAll();
        }
        finally {
            lock.unlock();
        }
    }

    // Lock must already be held
    protected void runUpdateAll() {
        _updateAll = true;
        while (!_counts.isEmpty()) {
            _updatesReady.signalAll();
            try {
                // Give up the lock for update thread, looping in case
                // it missed the signal.
                _updatesReady.await(250, TimeUnit.MILLISECONDS);
            }
            catch (InterruptedException e) {
                if (_exceptionListener != null) {
                    _exceptionListener.exceptionThrown(e);
                }
            }
        }
    }

    /**
     * Internal thread for asynchronously delegating recovery state updates
     * to the wrapper recovery point adapter.
     */
    protected class UpdateThread extends Thread {
        protected ArrayList _updates = new ArrayList();

        public UpdateThread(String threadName) {
            super(threadName);
        }

        /**
         * Entry point for the runnable. Retrieves the next subId from the
         * queue and writes an update to the adapter.
         */
        public void run() {
            boolean closed = _closed;
            boolean updateAll;
            while (!closed) {
                try {
                    lock.lock();
                    try {
                        // Pause here until we're told there are updates or it
                        // is time to check for delayed updates.
                        _updatesReady.await(_updateIntervalMillis, TimeUnit.MILLISECONDS);
                        updateAll = _updateAll;
                        if (!updateAll) {
                            long now = System.currentTimeMillis();
                            for (Map.Entry entry : _timers.entrySet()) {
                                if (entry.getValue() + _timeoutMillis >= now) {
                                    Field subId = entry.getKey();
                                    RecoveryPoint update = _latestUpdates.get(subId);
                                    if (update != null) {
                                        _latestUpdates.remove(subId);
                                        _updates.add(update);
                                    }
                                }
                            }
                        }
                        for (Map.Entry entry : _counts.entrySet()) {
                            if (updateAll
                                || entry.getValue() >= _updateThreshold) {
                                Field subId = entry.getKey();
                                RecoveryPoint update = _latestUpdates.get(subId);
                                if (update != null) {
                                    _latestUpdates.remove(subId);
                                    _updates.add(update);
                                }
                            }
                        }
                        for (RecoveryPoint rp : _updates) {
                            Field subId = rp.getSubId();
                            _counts.remove(subId);
                            _timers.remove(subId);
                        }
                        closed = _closed;
                    }
                    finally { lock.unlock(); }
                    if (updateAll) {
                        lock.lock();
                    }
                    try {
                        for (RecoveryPoint update : _updates) {
                            _adapter.update(update);
                        }
                    }
                    catch(Exception e) {
                        if (_exceptionListener != null) {
                            _exceptionListener.exceptionThrown(e);
                        }
                    }
                    finally {
                        if (updateAll) {
                            _updateAll = false;
                            _updatesReady.signalAll();
                            lock.unlock();
                        }
                    }
                    _updates.clear();
                } catch (Exception e) {
                    if (_exceptionListener != null) {
                        _exceptionListener.exceptionThrown(e);
                    }
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy