com.crossbusiness.resiliency.aspect.AbstractGovernorAspect.java Source code

Java tutorial

Introduction

Here is the source code for com.crossbusiness.resiliency.aspect.AbstractGovernorAspect.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c)  2014 CrossBusiness, Inc.
 *
 * 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 com.crossbusiness.resiliency.aspect;

import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import javax.annotation.PreDestroy;

import com.crossbusiness.resiliency.annotation.Governor;

import com.crossbusiness.resiliency.exception.ConcurrencyLimitExceededException;
import com.crossbusiness.resiliency.exception.RateLimitExceededException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Create a Semaphore with the same permits as the count per period of the throttle (eg: 2, in a 2 invocations per second throttle)
 * Create a Timer that runs as the throttle period and resets all the initial permits of the semaphore (eg: each second)
 * Before each invocation, just try to acquire the semaphore, if there are not permits available, it will block until the timer resets the permits and the method can proceed
 */
/**
 * @author Sumanth Chinthagunta <xmlking@gmail.com>
 *
 */
//@Aspect("pertarget(@annotation(throttleConfig))") //TODO to keep 'throttles' unique. This makes one aspect instance for each unique target object at matched join points. Standard @Aspect makes it singleton by default.
@Aspect
public abstract class AbstractGovernorAspect {
    static final Logger log = LoggerFactory.getLogger(AbstractGovernorAspect.class);

    private interface Throttler {
        public Object proceed(ProceedingJoinPoint pjp) throws Throwable;
    }

    private static class ConcurrencyThrottleDecorator implements Throttler {
        int limit;
        Semaphore semaphore;
        boolean blocking;

        public ConcurrencyThrottleDecorator(int limit, boolean blocking) {
            this.limit = limit;
            this.blocking = blocking;
            this.semaphore = new Semaphore(limit, true);
        }

        @Override
        public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
            if (blocking) {
                try {
                    semaphore.acquire();
                    return pjp.proceed();
                } finally {
                    semaphore.release();
                }
            } else {
                if (semaphore.tryAcquire()) {
                    try {
                        return pjp.proceed();
                    } finally {
                        semaphore.release();
                    }
                } else {
                    log.warn("Request rejected: concurrency limit {} exceeded", limit);
                    throw new ConcurrencyLimitExceededException(limit);
                }
            }
        }
    }

    private static class RateThrottleDecorator extends ConcurrencyThrottleDecorator {
        ScheduledFuture timer;

        public RateThrottleDecorator(final int limit, final long period, final TimeUnit unit,
                final boolean blocking, ScheduledExecutorService scheduler) {
            super(limit, blocking);
            this.timer = scheduler.scheduleAtFixedRate(new TimerTask() {
                public void run() {
                    log.debug(
                            "Throttler[limit={},period={},unit={},blocking={}] : {} permits available before release",
                            new Object[] { limit, period, unit, blocking, semaphore.availablePermits() });
                    synchronized (semaphore) {
                        semaphore.release(limit - semaphore.availablePermits());
                    }
                }
            }, 0, period, unit);
        }

        public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
            if (timer.isCancelled()) {
                throw new RuntimeException("Invalid timer!");
            }
            if (blocking) {
                semaphore.acquire();
                return pjp.proceed();
            } else {
                if (semaphore.tryAcquire()) {
                    return pjp.proceed();
                } else {
                    log.warn("Request rejected: rate limit {} exceeded", limit);
                    throw new RateLimitExceededException(limit);
                }
            }
        }
    }

    Map<String, Throttler> throttles = new ConcurrentHashMap<String, Throttler>();
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    @Around("governorAnnotatedClass(governorConfig)")
    public Object governorOnClassLevel(final ProceedingJoinPoint pjp, Governor governorConfig) throws Throwable {
        return wrap(pjp, governorConfig);
    }

    @Around("governorAnnotatedMethod(governorConfig)")
    public Object governorOnMethodLevel(final ProceedingJoinPoint pjp, Governor governorConfig) throws Throwable {
        return wrap(pjp, governorConfig);
    }

    // @Around("governorMethodExecution(governorConfig)")
    protected Object wrap(final ProceedingJoinPoint pjp, Governor governorConfig) throws Throwable {
        // throttle per instance, per method
        String throttleKey = pjp.getTarget().hashCode() + pjp.getSignature().toLongString();

        if (!throttles.containsKey(throttleKey)) {
            switch (governorConfig.type()) {
            case CONCURRENCY:
                throttles.put(throttleKey,
                        new ConcurrencyThrottleDecorator(governorConfig.limit(), governorConfig.blocking()));
                break;
            case RATE:
                throttles.put(throttleKey, new RateThrottleDecorator(governorConfig.limit(),
                        governorConfig.period(), governorConfig.unit(), governorConfig.blocking(), scheduler));
                break;
            case ALL: //TODO
                break;
            default:
                log.error("Unknown Throttler type");
                break;
            }
        }

        Throttler t = throttles.get(throttleKey);
        if (t != null) {
            return t.proceed(pjp);
        } else {
            log.error("no throttles found");
            return pjp.proceed();
        }

    }

    @PreDestroy
    public void shutdown() {
        scheduler.shutdownNow();
    }

    @Pointcut
    public abstract void governorAnnotatedMethod(Governor governorConfig);

    @Pointcut
    public abstract void governorAnnotatedClass(Governor governorConfig);

    @Pointcut
    public abstract void governorMethodExecution(Governor governorConfig);
}