/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.engine.view.client.merging; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import com.opengamma.engine.resource.EngineResourceManagerInternal; import com.opengamma.engine.view.listener.ViewResultListener; import com.opengamma.util.ArgumentChecker; /** * Merges view process results to satisfy a specified maximum downstream update rate (given in terms of a minimum period between updates). This maximum rate can be adjusted on-the-fly. */ public class RateLimitingMergingViewProcessListener extends MergingViewProcessListener { private static final long MIN_PERIOD = 50; private final ScheduledExecutorService _timer; private ReentrantLock _taskSetupLock = new ReentrantLock(); private Future<?> _asyncUpdateCheckerTask; private boolean _isPaused; private AtomicLong _minimumUpdatePeriodMillis = new AtomicLong(0); /** * The time at which an update was last triggered. */ private AtomicLong _lastUpdateTimeMillis = new AtomicLong(); public RateLimitingMergingViewProcessListener(ViewResultListener underlying, EngineResourceManagerInternal<?> cycleManager, ScheduledExecutorService timer) { super(underlying, cycleManager); ArgumentChecker.notNull(timer, "timer"); _timer = timer; } public void terminate() { _taskSetupLock.lock(); try { cancelTimerTask(); } finally { _taskSetupLock.unlock(); } } //------------------------------------------------------------------------- public boolean isPaused() { return _isPaused; } /** * Sets whether output from the provider is paused. While it is paused, updates are merged into a single update which is released when the provider is resumed. * * @param isPaused <code>true</code> to indicate that output should be paused, or <code>false</code> to indicate that output should flow normally according to the update rate. */ public void setPaused(boolean isPaused) { final Call<?> drain; _taskSetupLock.lock(); try { if (_isPaused == isPaused) { return; } _isPaused = isPaused; drain = updateConfiguration(); } finally { _taskSetupLock.unlock(); } invoke(drain); } //------------------------------------------------------------------------- /** * Gets the minimum period which must have elapsed since the last update before an update is triggered. * * @return the minimum period which must have elapsed since the last update before an update is triggered, in milliseconds */ public long getMinimumUpdatePeriodMillis() { return _minimumUpdatePeriodMillis.get(); } /** * Sets the minimum period which must have elapsed since the last update before an update is triggered. The value given is only a minimum, and the actual period between updates may be higher. If * more frequent updates are required then consider using a pass-through provider instead. * * @param minimumUpdatePeriodMillis the minimum period which must have elapsed since the last update before an update is triggered, in milliseconds. If 0, updates will be passed to listeners * immediately and synchronously (unless paused). */ public void setMinimumUpdatePeriodMillis(long minimumUpdatePeriodMillis) { final Call<?> drain; _taskSetupLock.lock(); try { if (minimumUpdatePeriodMillis <= 0) { // No merging minimumUpdatePeriodMillis = 0; } else { // Merging at no less than the minimum period minimumUpdatePeriodMillis = Math.max(MIN_PERIOD, minimumUpdatePeriodMillis); } _minimumUpdatePeriodMillis.set(minimumUpdatePeriodMillis); drain = updateConfiguration(); } finally { _taskSetupLock.unlock(); } invoke(drain); } //------------------------------------------------------------------------- private boolean drainIfRequired() { long currentTime = System.currentTimeMillis(); long lastUpdateTime = _lastUpdateTimeMillis.get(); long lastResultTime = getLastUpdateTimeMillis(); if (lastResultTime < lastUpdateTime) { // No more results since the last output return false; } long minimumUpdatePeriodMillis = getMinimumUpdatePeriodMillis(); if (currentTime - lastUpdateTime < minimumUpdatePeriodMillis) { return false; } if (!_lastUpdateTimeMillis.compareAndSet(lastUpdateTime, currentTime)) { // Another thread has got there before us return false; } drain(); return true; } private Call<?> updateConfiguration() { long minimumUpdatePeriodMillis = getMinimumUpdatePeriodMillis(); cancelTimerTask(); final Call<?> drain = setPassThrough(minimumUpdatePeriodMillis == 0 && !isPaused()); if (!isPaused() && !isPassThrough()) { final Runnable task = new Runnable() { @Override public void run() { drainIfRequired(); } }; _asyncUpdateCheckerTask = _timer.scheduleWithFixedDelay(task, minimumUpdatePeriodMillis, minimumUpdatePeriodMillis, TimeUnit.MILLISECONDS); } return drain; } private void cancelTimerTask() { if (_asyncUpdateCheckerTask != null) { _asyncUpdateCheckerTask.cancel(true); _asyncUpdateCheckerTask = null; } } }