com.adaptris.core.interceptor.ThrottlingInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for com.adaptris.core.interceptor.ThrottlingInterceptor.java

Source

/*
 * Copyright 2015 Adaptris Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

package com.adaptris.core.interceptor;

import static org.apache.commons.lang.StringUtils.isEmpty;

import java.util.Calendar;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adaptris.annotation.AutoPopulated;
import com.adaptris.annotation.ComponentProfile;
import com.adaptris.annotation.DisplayOrder;
import com.adaptris.core.AdaptrisMessage;
import com.adaptris.core.CoreException;
import com.adaptris.core.util.ManagedThreadFactory;
import com.adaptris.util.TimeInterval;
import com.thoughtworks.xstream.annotations.XStreamAlias;

/**
 * The throttling interceptor will attempt to limit the number of messages that are processed by an adapter over a given time slot.
 * <p>
 * A time slice is measured through the {@link #setTimeSliceInterval(TimeInterval)}. Once the first message is processed by this
 * interceptor, a time slice is created. The start of the time slice is the current time, the end of the time slice is set as the
 * current time + the number of time units in the configuration. Every message processed by the interceptor will start a check to
 * see if the current time slice is out of date. If it is out of date, then a new time slice is created. If the current time slice
 * is not out of date, then we check to make sure the maximum messages have not already passed through. If we have not yet reached
 * the maximum messages for the current time slice, we simply increment the message count on the time slice. If we have reached the
 * maximum messages, then we will delay this processing thread until the current time slice is finished, and finally a new time
 * slice is created.
 * </p>
 * <p>
 * Any given instance of the ThrottlingInterceptor can be set to work with a particular cache name. This allows you to configure a
 * throttling interceptor on one or more workflows to all use the same time slice or use a completely different time slice.
 * </p>
 * <p>
 * For example, you can set workflow A and workflow B to both have a throttling interceptor, where both interceptors use the same
 * cache name. This will have the effect of restricting the sum of messages passing through both workflows against the same time
 * slice statistics.
 * </p>
 * <p>
 * Likewise, you may have a third workflow with an interceptor configured to use a different cache name. This means messages passing
 * through the third workflow will not have the same throttling restrictions as those on workflow A and B.
 * </p>
 * The following properties may be set in configuration;
 * <ul>
 * <li>time-slice-interval - This is the number of time units each time slice will use. The default value is 5.</li>
 * <li>maximum-messages - The maximum amount of messages allowed to pass through the current time slice The default value is
 * {@link Integer#MAX_VALUE}</li>
 * <li>cache-provider - The implementation that handles the time slice memory management. The default is
 * {@link TimeSliceDefaultCacheProvider}</li>
 * <li>cache-name - This allows you to set the cache name that the time slice statistics will be created/used from;<strong>this must
 * be specified</strong></li>
 * </ul>
 * 
 * @config throttling-interceptor
 * 
 * @author amcgrath
 */
@XStreamAlias("throttling-interceptor")
@ComponentProfile(summary = "Throttles message flow based on some message count criteria", tag = "interceptor,jmx")
@DisplayOrder(order = { "maximumMessages", "timeSliceInterval" })
public class ThrottlingInterceptor extends WorkflowInterceptorImpl {

    private transient Logger log = LoggerFactory.getLogger(this.getClass().getName());

    private static final int MAXIMUM_MESSAGES_DEFAULT = Integer.MAX_VALUE;
    private static final TimeInterval DEFAULT_TIMESLICE_INTERVAL = new TimeInterval(5L, TimeUnit.SECONDS);

    private TimeInterval timeSliceInterval;
    private int maximumMessages;

    private transient ExecutorService executor;

    @Valid
    @NotNull
    @AutoPopulated
    private TimeSliceCacheProvider cacheProvider;
    /**
     * Required.
     */
    @NotBlank
    private String cacheName;

    public ThrottlingInterceptor() {
        super();
        maximumMessages = MAXIMUM_MESSAGES_DEFAULT;
        cacheProvider = new TimeSliceDefaultCacheProvider();
    }

    @Override
    public void init() throws CoreException {
        if (isEmpty(cacheName)) {
            throw new CoreException("Cache Name not specified.");
        }

        ((TimeSliceAbstractCacheProvider) cacheProvider).setTimeSliceDurationMilliseconds(getMillisecondDuration());
        getCacheProvider().init();
        executor = Executors.newSingleThreadExecutor(new ManagedThreadFactory(getClass().getSimpleName()));
    }

    @Override
    public void start() throws CoreException {
        getCacheProvider().start();
    }

    @Override
    public void stop() {
        getCacheProvider().stop();
    }

    @Override
    public void close() {
        ManagedThreadFactory.shutdownQuietly(executor, new TimeInterval());
    }

    //
    @Override
    public synchronized void workflowStart(AdaptrisMessage inputMsg) {
        // CacheProvider.get() will always return a timeslice - can never be null.
        TimeSlice currentTimeSlice = getCacheProvider().get(getCacheName());

        log.debug("Using timeslice - " + currentTimeSlice.toString());
        if (currentTimeSlice.getTotalMessageCount() >= getMaximumMessages()) {
            long currentTime = Calendar.getInstance().getTimeInMillis();
            long delayUntil = currentTimeSlice.getEndMillis();

            long delayFor = Math.abs(delayUntil - currentTime);
            log.debug("Delaying thread (Throttling) for {} ms", delayFor);
            if (throttle(delayFor))
                workflowStart(inputMsg);
        } else {
            currentTimeSlice.setTotalMessageCount(currentTimeSlice.getTotalMessageCount() + 1);
        }
    }

    // returns true if the throttle was successfully applied
    private boolean throttle(final long delay) {
        final CyclicBarrier barrier = new CyclicBarrier(2);
        // This is to make sure we don't break the barrier before the real delay is up.
        // 
        long barrierDelay = delay + TimeUnit.SECONDS.toMillis(1L);
        boolean result = true;
        try {
            executor.execute(new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(delay);
                        barrier.await(TimeUnit.SECONDS.toMillis(1L), TimeUnit.MILLISECONDS);
                    } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
                        log.trace("Interrupted during sleep; breaking barrier");
                        barrier.reset();
                    }
                }

            });
            barrier.await(barrierDelay, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
            result = false;
        }
        return result;
    }

    private long getMillisecondDuration() {
        return TimeInterval.toMillisecondsDefaultIfNull(getTimeSliceInterval(), DEFAULT_TIMESLICE_INTERVAL);
    }

    @Override
    public void workflowEnd(AdaptrisMessage inputMsg, AdaptrisMessage outputMsg) {
    }

    public int getMaximumMessages() {
        return maximumMessages;
    }

    /**
     * Set the maximum number of messages that can be processed in each timeslice.
     * 
     * @param maximumMessages the max number of messages, default is {@value java.lang.Integer#MAX_VALUE}
     */
    public void setMaximumMessages(int maximumMessages) {
        this.maximumMessages = maximumMessages;
    }

    public TimeSliceCacheProvider getCacheProvider() {
        return cacheProvider;
    }

    /**
     * Set the caching provider for timeslices.
     * 
     * @param cacheProvider the caching provider.
     */
    public void setCacheProvider(TimeSliceCacheProvider cacheProvider) {
        this.cacheProvider = cacheProvider;
    }

    public String getCacheName() {
        return cacheName;
    }

    /**
     * Set the cache name for shared throttles.
     * 
     * @param cacheName the cache name.
     */
    public void setCacheName(String cacheName) {
        this.cacheName = cacheName;
    }

    /**
     * Set the interval for each timeslice.
     * 
     * @param interval
     */
    public void setTimeSliceInterval(TimeInterval interval) {
        timeSliceInterval = interval;
    }

    public TimeInterval getTimeSliceInterval() {
        return timeSliceInterval;
    }
}