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

org.apache.camel.throttling.ThrottlingExceptionRoutePolicy Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.throttling;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.Route;
import org.apache.camel.RouteAware;
import org.apache.camel.spi.CamelLogger;
import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RoutePolicy;
import org.apache.camel.support.RoutePolicySupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Modeled after the circuit breaker {@link ThrottlingInflightRoutePolicy} this {@link RoutePolicy} will stop consuming
 * from an endpoint based on the type of exceptions that are thrown and the threshold setting.
 *
 * The scenario: if a route cannot process data from an endpoint due to problems with resources used by the route (ie
 * database down) then it will stop consuming new messages from the endpoint by stopping the consumer. The
 * implementation is comparable to the Circuit Breaker pattern. After a set amount of time, it will move to a half open
 * state and attempt to determine if the consumer can be started. There are two ways to determine if a route can be
 * closed after being opened (1) start the consumer and check the failure threshold (2) call the
 * {@link ThrottlingExceptionHalfOpenHandler} The second option allows a custom check to be performed without having to
 * take on the possibility of multiple messages from the endpoint. The idea is that a handler could run a simple test
 * (ie select 1 from dual) to determine if the processes that cause the route to be open are now available
 */
@Metadata(label = "bean",
          description = "A throttle based RoutePolicy which is modelled after the circuit breaker and will stop consuming"
                        + " from an endpoint based on the type of exceptions that are thrown and the threshold settings.",
          annotations = { "interfaceName=org.apache.camel.spi.RoutePolicy" })
@Configurer(metadataOnly = true)
public class ThrottlingExceptionRoutePolicy extends RoutePolicySupport implements CamelContextAware, RouteAware {

    private static final Logger LOG = LoggerFactory.getLogger(ThrottlingExceptionRoutePolicy.class);

    private static final int STATE_CLOSED = 0;
    private static final int STATE_HALF_OPEN = 1;
    private static final int STATE_OPEN = 2;

    private CamelContext camelContext;
    private Route route;
    private final Lock lock = new ReentrantLock();
    private CamelLogger stateLogger;

    // configuration
    @Metadata(description = "How many failed messages within the window would trigger the circuit breaker to open",
              defaultValue = "50")
    private int failureThreshold = 50;
    @Metadata(description = "Sliding window for how long time to go back (in millis) when counting number of failures",
              defaultValue = "60000")
    private long failureWindow = 60000;
    @Metadata(description = "Interval (in millis) for how often to check whether a currently open circuit breaker may work again",
              defaultValue = "30000")
    private long halfOpenAfter = 30000;
    @Metadata(description = "Whether to always keep the circuit breaker open (never closes). This is only intended for development and testing purposes.")
    private boolean keepOpen;
    @Metadata(description = "Allows to only throttle based on certain types of exceptions. Multiple exceptions (use FQN class name) can be separated by comma.")
    private String exceptions;
    @Metadata(description = "Logging level for state changes", defaultValue = "DEBUG")
    private LoggingLevel stateLoggingLevel = LoggingLevel.DEBUG;
    private List> throttledExceptions;
    // handler for half open circuit can be used instead of resuming route to check on resources
    @Metadata(label = "advanced",
              description = "Custom check to perform whether the circuit breaker can move to half-open state."
                            + " If set then this is used instead of resuming the route.")
    private ThrottlingExceptionHalfOpenHandler halfOpenHandler;

    // stateful information
    private final AtomicInteger failures = new AtomicInteger();
    private final AtomicInteger success = new AtomicInteger();
    private final AtomicInteger state = new AtomicInteger(STATE_CLOSED);
    private final AtomicBoolean keepOpenBool = new AtomicBoolean();
    private volatile Timer halfOpenTimer;
    private volatile long lastFailure;
    private volatile long openedAt;

    public ThrottlingExceptionRoutePolicy() {
    }

    public ThrottlingExceptionRoutePolicy(int threshold, long failureWindow, long halfOpenAfter,
                                          List> handledExceptions) {
        this(threshold, failureWindow, halfOpenAfter, handledExceptions, false);
    }

    public ThrottlingExceptionRoutePolicy(int threshold, long failureWindow, long halfOpenAfter,
                                          List> handledExceptions, boolean keepOpen) {
        this.throttledExceptions = handledExceptions;
        this.failureWindow = failureWindow;
        this.halfOpenAfter = halfOpenAfter;
        this.failureThreshold = threshold;
        this.keepOpenBool.set(keepOpen);
    }

    @Override
    public void setCamelContext(CamelContext camelContext) {
        this.camelContext = camelContext;
    }

    @Override
    public CamelContext getCamelContext() {
        return camelContext;
    }

    @Override
    public Route getRoute() {
        return route;
    }

    @Override
    public void setRoute(Route route) {
        this.route = route;
    }

    public List> getThrottledExceptions() {
        return throttledExceptions;
    }

    public String getExceptions() {
        return exceptions;
    }

    public void setExceptions(String exceptions) {
        this.exceptions = exceptions;
    }

    @Override
    protected void doInit() throws Exception {
        super.doInit();

        this.stateLogger = new CamelLogger(LOG, stateLoggingLevel);

        if (exceptions != null && throttledExceptions == null) {
            var list = new ArrayList>();
            for (String fqn : exceptions.split(",")) {
                Class clazz = camelContext.getClassResolver().resolveMandatoryClass(fqn);
                list.add(clazz);
            }
            this.throttledExceptions = list;
        }
    }

    @Override
    public void onInit(Route route) {
        LOG.debug("Initializing ThrottlingExceptionRoutePolicy route policy");
        logState();
    }

    @Override
    public void onStart(Route route) {
        // if keepOpen then start w/ the circuit open
        if (keepOpenBool.get()) {
            openCircuit(route);
        }
    }

    @Override
    protected void doStop() throws Exception {
        Timer timer = halfOpenTimer;
        if (timer != null) {
            timer.cancel();
            halfOpenTimer = null;
        }
    }

    @Override
    public void onExchangeDone(Route route, Exchange exchange) {
        if (keepOpenBool.get()) {
            if (state.get() != STATE_OPEN) {
                LOG.debug("Opening circuit (keepOpen is true)");
                openCircuit(route);
            }
        } else {
            if (hasFailed(exchange)) {
                // record the failure
                failures.incrementAndGet();
                lastFailure = System.currentTimeMillis();
            } else {
                success.incrementAndGet();
            }

            // check for state change
            calculateState(route);
        }
    }

    /**
     * Uses similar approach as circuit breaker if the exchange has an exception that we are watching then we count that
     * as a failure otherwise we ignore it
     */
    private boolean hasFailed(Exchange exchange) {
        if (exchange == null) {
            return false;
        }

        boolean answer = false;

        if (exchange.getException() != null) {
            if (throttledExceptions == null || throttledExceptions.isEmpty()) {
                // if no exceptions defined then always fail
                // (ie) assume we throttle on all exceptions
                answer = true;
            } else {
                for (Class exception : throttledExceptions) {
                    // will look in exception hierarchy
                    if (exchange.getException(exception) != null) {
                        answer = true;
                        break;
                    }
                }
            }
        }

        if (LOG.isDebugEnabled()) {
            String exceptionName
                    = exchange.getException() == null ? "none" : exchange.getException().getClass().getSimpleName();
            LOG.debug("hasFailed ({}) with Throttled Exception: {} for exchangeId: {}", answer, exceptionName,
                    exchange.getExchangeId());
        }
        return answer;
    }

    private void calculateState(Route route) {

        // have we reached the failure limit?
        boolean failureLimitReached = isThresholdExceeded();

        if (state.get() == STATE_CLOSED) {
            if (failureLimitReached) {
                LOG.debug("Opening circuit...");
                openCircuit(route);
            }
        } else if (state.get() == STATE_HALF_OPEN) {
            if (failureLimitReached) {
                LOG.debug("Opening circuit...");
                openCircuit(route);
            } else {
                LOG.debug("Closing circuit...");
                closeCircuit(route);
            }
        } else if (state.get() == STATE_OPEN) {
            if (!keepOpenBool.get()) {
                long elapsedTimeSinceOpened = System.currentTimeMillis() - openedAt;
                if (halfOpenAfter <= elapsedTimeSinceOpened) {
                    LOG.debug("Checking an open circuit...");
                    if (halfOpenHandler != null) {
                        if (halfOpenHandler.isReadyToBeClosed()) {
                            LOG.debug("Closing circuit...");
                            closeCircuit(route);
                        } else {
                            LOG.debug("Opening circuit...");
                            openCircuit(route);
                        }
                    } else {
                        LOG.debug("Half opening circuit...");
                        halfOpenCircuit(route);
                    }
                } else {
                    LOG.debug("Keeping circuit open (time not elapsed)...");
                }
            } else {
                LOG.debug("Keeping circuit open (keepOpen is true)...");
                this.addHalfOpenTimer(route);
            }
        }

    }

    protected boolean isThresholdExceeded() {
        boolean output = false;
        logState();
        // failures exceed the threshold
        // AND the last of those failures occurred within window
        if (failures.get() >= failureThreshold && lastFailure >= System.currentTimeMillis() - failureWindow) {
            output = true;
        }

        return output;
    }

    protected void openCircuit(Route route) {
        try {
            lock.lock();
            suspendOrStopConsumer(route.getConsumer());
            state.set(STATE_OPEN);
            openedAt = System.currentTimeMillis();
            this.addHalfOpenTimer(route);
            logState();
        } catch (Exception e) {
            handleException(e);
        } finally {
            lock.unlock();
        }
    }

    protected void addHalfOpenTimer(Route route) {
        halfOpenTimer = new Timer();
        halfOpenTimer.schedule(new HalfOpenTask(route), halfOpenAfter);
    }

    protected void halfOpenCircuit(Route route) {
        try {
            lock.lock();
            resumeOrStartConsumer(route.getConsumer());
            state.set(STATE_HALF_OPEN);
            logState();
        } catch (Exception e) {
            handleException(e);
        } finally {
            lock.unlock();
        }
    }

    protected void closeCircuit(Route route) {
        try {
            lock.lock();
            resumeOrStartConsumer(route.getConsumer());
            failures.set(0);
            success.set(0);
            lastFailure = 0;
            openedAt = 0;
            state.set(STATE_CLOSED);
            logState();
        } catch (Exception e) {
            handleException(e);
        } finally {
            lock.unlock();
        }
    }

    private void logState() {
        if (stateLogger != null) {
            stateLogger.log(dumpState());
        }
    }

    public String getStateAsString() {
        return stateAsString(state.get());
    }

    public String dumpState() {
        String routeState = getStateAsString();
        if (failures.get() > 0) {
            return String.format("State %s, failures %d, last failure %d ms ago", routeState, failures.get(),
                    System.currentTimeMillis() - lastFailure);
        } else {
            return String.format("State %s, failures %d", routeState, failures.get());
        }
    }

    private static String stateAsString(int num) {
        if (num == STATE_CLOSED) {
            return "closed";
        } else if (num == STATE_HALF_OPEN) {
            return "half opened";
        } else {
            return "opened";
        }
    }

    class HalfOpenTask extends TimerTask {
        private final Route route;

        HalfOpenTask(Route route) {
            this.route = route;
        }

        @Override
        public void run() {
            if (halfOpenTimer != null) {
                halfOpenTimer.cancel();
            }
            calculateState(route);
        }
    }

    public ThrottlingExceptionHalfOpenHandler getHalfOpenHandler() {
        return halfOpenHandler;
    }

    public void setHalfOpenHandler(ThrottlingExceptionHalfOpenHandler halfOpenHandler) {
        this.halfOpenHandler = halfOpenHandler;
    }

    public boolean getKeepOpen() {
        return this.keepOpenBool.get();
    }

    public void setKeepOpen(boolean keepOpen) {
        this.keepOpenBool.set(keepOpen);
    }

    public int getFailureThreshold() {
        return failureThreshold;
    }

    public void setFailureThreshold(int failureThreshold) {
        this.failureThreshold = failureThreshold;
    }

    public long getFailureWindow() {
        return failureWindow;
    }

    public void setFailureWindow(long failureWindow) {
        this.failureWindow = failureWindow;
    }

    public long getHalfOpenAfter() {
        return halfOpenAfter;
    }

    public void setHalfOpenAfter(long halfOpenAfter) {
        this.halfOpenAfter = halfOpenAfter;
    }

    public int getFailures() {
        return failures.get();
    }

    public int getSuccess() {
        return success.get();
    }

    public long getLastFailure() {
        return lastFailure;
    }

    public long getOpenedAt() {
        return openedAt;
    }

    public LoggingLevel getStateLoggingLevel() {
        return stateLoggingLevel;
    }

    public void setStateLoggingLevel(LoggingLevel stateLoggingLevel) {
        this.stateLoggingLevel = stateLoggingLevel;
        if (stateLogger != null) {
            stateLogger.setLevel(stateLoggingLevel);
        }
    }

    public void setStateLoggingLevel(String stateLoggingLevel) {
        setStateLoggingLevel(LoggingLevel.valueOf(stateLoggingLevel));
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy