001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.processor;
018
019import java.util.concurrent.DelayQueue;
020import java.util.concurrent.Delayed;
021import java.util.concurrent.ExecutorService;
022import java.util.concurrent.RejectedExecutionException;
023import java.util.concurrent.TimeUnit;
024
025import org.apache.camel.AsyncCallback;
026import org.apache.camel.CamelContext;
027import org.apache.camel.Exchange;
028import org.apache.camel.Expression;
029import org.apache.camel.Processor;
030import org.apache.camel.RuntimeExchangeException;
031import org.apache.camel.Traceable;
032import org.apache.camel.spi.IdAware;
033import org.apache.camel.util.AsyncProcessorHelper;
034import org.apache.camel.util.ObjectHelper;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * A <a href="http://camel.apache.org/throttler.html">Throttler</a>
040 * will set a limit on the maximum number of message exchanges which can be sent
041 * to a processor within a specific time period. <p/> This pattern can be
042 * extremely useful if you have some external system which meters access; such
043 * as only allowing 100 requests per second; or if huge load can cause a
044 * particular system to malfunction or to reduce its throughput you might want
045 * to introduce some throttling.
046 *
047 * This throttle implementation is thread-safe and is therefore safe to be used
048 * by multiple concurrent threads in a single route.
049 *
050 * The throttling mechanism is a DelayQueue with maxRequestsPerPeriod permits on
051 * it. Each permit is set to be delayed by timePeriodMillis (except when the
052 * throttler is initialized or the throttle rate increased, then there is no delay
053 * for those permits). Callers trying to acquire a permit from the DelayQueue will
054 * block if necessary. The end result is a rolling window of time. Where from the
055 * callers point of view in the last timePeriodMillis no more than
056 * maxRequestsPerPeriod have been allowed to be acquired.
057 *
058 * @version
059 */
060public class Throttler extends DelegateAsyncProcessor implements Traceable, IdAware {
061
062    private static final String PROPERTY_EXCHANGE_QUEUED_TIMESTAMP = "CamelThrottlerExchangeQueuedTimestamp";
063    private static final String PROPERTY_EXCHANGE_STATE = "CamelThrottlerExchangeState";
064
065    private enum State { SYNC, ASYNC, ASYNC_REJECTED }
066
067    private final Logger log = LoggerFactory.getLogger(Throttler.class);
068    private final CamelContext camelContext;
069    private final DelayQueue<ThrottlePermit> delayQueue = new DelayQueue<>();
070    private final ExecutorService asyncExecutor;
071    private final boolean shutdownAsyncExecutor;
072
073    private volatile long timePeriodMillis;
074    private volatile int throttleRate;
075    private String id;
076    private Expression maxRequestsPerPeriodExpression;
077    private boolean rejectExecution;
078    private boolean asyncDelayed;
079    private boolean callerRunsWhenRejected = true;
080
081    public Throttler(final CamelContext camelContext, final Processor processor, final Expression maxRequestsPerPeriodExpression, final long timePeriodMillis,
082                     final ExecutorService asyncExecutor, final boolean shutdownAsyncExecutor, final boolean rejectExecution) {
083        super(processor);
084        this.camelContext = camelContext;
085        this.rejectExecution = rejectExecution;
086        this.shutdownAsyncExecutor = shutdownAsyncExecutor;
087
088        ObjectHelper.notNull(maxRequestsPerPeriodExpression, "maxRequestsPerPeriodExpression");
089        this.maxRequestsPerPeriodExpression = maxRequestsPerPeriodExpression;
090
091        if (timePeriodMillis <= 0) {
092            throw new IllegalArgumentException("TimePeriodMillis should be a positive number, was: " + timePeriodMillis);
093        }
094        this.timePeriodMillis = timePeriodMillis;
095        this.asyncExecutor = asyncExecutor;
096    }
097
098    @Override
099    public boolean process(final Exchange exchange, final AsyncCallback callback) {
100        long queuedStart = 0;
101        if (log.isTraceEnabled()) {
102            queuedStart = exchange.getProperty(PROPERTY_EXCHANGE_QUEUED_TIMESTAMP, 0L, Long.class);
103            exchange.removeProperty(PROPERTY_EXCHANGE_QUEUED_TIMESTAMP);
104        }
105        State state = exchange.getProperty(PROPERTY_EXCHANGE_STATE, State.SYNC, State.class);
106        exchange.removeProperty(PROPERTY_EXCHANGE_STATE);
107        boolean doneSync = state == State.SYNC || state == State.ASYNC_REJECTED;
108
109        try {
110            if (!isRunAllowed()) {
111                throw new RejectedExecutionException("Run is not allowed");
112            }
113
114            calculateAndSetMaxRequestsPerPeriod(exchange);
115            ThrottlePermit permit = delayQueue.poll();
116
117            if (permit == null) {
118                if (isRejectExecution()) {
119                    throw new ThrottlerRejectedExecutionException("Exceeded the max throttle rate of "
120                            + throttleRate + " within " + timePeriodMillis + "ms");
121                } else {
122                    // delegate to async pool
123                    if (isAsyncDelayed() && !exchange.isTransacted() && state == State.SYNC) {
124                        log.debug("Throttle rate exceeded but AsyncDelayed enabled, so queueing for async processing, exchangeId: {}", exchange.getExchangeId());
125                        return processAsynchronously(exchange, callback);
126                    }
127
128                    // block waiting for a permit
129                    long start = 0;
130                    long elapsed = 0;
131                    if (log.isTraceEnabled()) {
132                        start = System.currentTimeMillis();
133                    }
134                    permit = delayQueue.take();
135                    if (log.isTraceEnabled()) {
136                        elapsed = System.currentTimeMillis() - start;
137                    }
138                    enqueuePermit(permit, exchange);
139
140                    if (state == State.ASYNC) {
141                        if (log.isTraceEnabled()) {
142                            long queuedTime = start - queuedStart;
143                            log.trace("Queued for {}ms, Throttled for {}ms, exchangeId: {}", queuedTime, elapsed, exchange.getExchangeId());
144                        }
145                    } else {
146                        log.trace("Throttled for {}ms, exchangeId: {}", elapsed, exchange.getExchangeId());
147                    }
148                }
149            } else {
150                enqueuePermit(permit, exchange);
151
152                if (state == State.ASYNC) {
153                    if (log.isTraceEnabled()) {
154                        long queuedTime = System.currentTimeMillis() - queuedStart;
155                        log.trace("Queued for {}ms, No throttling applied (throttle cleared while queued), for exchangeId: {}", queuedTime, exchange.getExchangeId());
156                    }
157                } else {
158                    log.trace("No throttling applied to exchangeId: {}", exchange.getExchangeId());
159                }
160            }
161
162            if (processor != null) {
163                if (doneSync) {
164                    return processor.process(exchange, callback);
165                } else {
166                    // if we are executing async, then we have to call the nested processor synchronously, and we
167                    // must not share our AsyncCallback, because the nested processing has no way of knowing that
168                    // we are already executing asynchronously.
169                    AsyncProcessorHelper.process(processor, exchange);
170                }
171            }
172
173            callback.done(doneSync);
174            return doneSync;
175
176        } catch (final InterruptedException e) {
177            // determine if we can still run, or the camel context is forcing a shutdown
178            boolean forceShutdown = exchange.getContext().getShutdownStrategy().forceShutdown(this);
179            if (forceShutdown) {
180                String msg = "Run not allowed as ShutdownStrategy is forcing shutting down, will reject executing exchange: " + exchange;
181                log.debug(msg);
182                exchange.setException(new RejectedExecutionException(msg, e));
183            } else {
184                exchange.setException(e);
185            }
186            callback.done(doneSync);
187            return doneSync;
188        } catch (final Throwable t) {
189            exchange.setException(t);
190            callback.done(doneSync);
191            return doneSync;
192        }
193    }
194
195    /**
196     * Delegate blocking on the DelayQueue to an asyncExecutor. Except if the executor rejects the submission
197     * and isCallerRunsWhenRejected() is enabled, then this method will delegate back to process(), but not
198     * before changing the exchange state to stop any recursion.
199     */
200    protected boolean processAsynchronously(final Exchange exchange, final AsyncCallback callback) {
201        try {
202            if (log.isTraceEnabled()) {
203                exchange.setProperty(PROPERTY_EXCHANGE_QUEUED_TIMESTAMP, System.currentTimeMillis());
204            }
205            exchange.setProperty(PROPERTY_EXCHANGE_STATE, State.ASYNC);
206            asyncExecutor.submit(new Runnable() {
207                @Override
208                public void run() {
209                    process(exchange, callback);
210                }
211            });
212            return false;
213        } catch (final RejectedExecutionException e) {
214            if (isCallerRunsWhenRejected()) {
215                log.debug("AsyncExecutor is full, rejected exchange will run in the current thread, exchangeId: {}", exchange.getExchangeId());
216                exchange.setProperty(PROPERTY_EXCHANGE_STATE, State.ASYNC_REJECTED);
217                return process(exchange, callback);
218            }
219            throw e;
220        }
221    }
222
223    /**
224     * Returns a permit to the DelayQueue, first resetting it's delay to be relative to now.
225     */
226    protected void enqueuePermit(final ThrottlePermit permit, final Exchange exchange) {
227        permit.setDelayMs(getTimePeriodMillis());
228        delayQueue.put(permit);
229        // try and incur the least amount of overhead while releasing permits back to the queue
230        if (log.isTraceEnabled()) {
231            log.trace("Permit released, for exchangeId: {}", exchange.getExchangeId());
232        }
233    }
234
235    /**
236     * Evaluates the maxRequestsPerPeriodExpression and adjusts the throttle rate up or down.
237     */
238    protected void calculateAndSetMaxRequestsPerPeriod(final Exchange exchange) throws Exception {
239        Integer newThrottle = maxRequestsPerPeriodExpression.evaluate(exchange, Integer.class);
240
241        if (newThrottle != null && newThrottle < 0) {
242            throw new IllegalStateException("The maximumRequestsPerPeriod must be a positive number, was: " + newThrottle);
243        }
244
245        synchronized (this) {
246            if (newThrottle == null && throttleRate == 0) {
247                throw new RuntimeExchangeException("The maxRequestsPerPeriodExpression was evaluated as null: " + maxRequestsPerPeriodExpression, exchange);
248            }
249
250            if (newThrottle != null) {
251                if (newThrottle != throttleRate) {
252                    // decrease
253                    if (throttleRate > newThrottle) {
254                        int delta = throttleRate - newThrottle;
255
256                        // discard any permits that are needed to decrease throttling
257                        while (delta > 0) {
258                            delayQueue.take();
259                            delta--;
260                            log.trace("Permit discarded due to throttling rate decrease, triggered by ExchangeId: {}", exchange.getExchangeId());
261                        }
262                        log.debug("Throttle rate decreased from {} to {}, triggered by ExchangeId: {}", throttleRate, newThrottle, exchange.getExchangeId());
263
264                    // increase
265                    } else if (newThrottle > throttleRate) {
266                        int delta = newThrottle - throttleRate;
267                        for (int i = 0; i < delta; i++) {
268                            delayQueue.put(new ThrottlePermit(-1));
269                        }
270                        if (throttleRate == 0) {
271                            log.debug("Initial throttle rate set to {}, triggered by ExchangeId: {}", newThrottle, exchange.getExchangeId());
272                        } else {
273                            log.debug("Throttle rate increase from {} to {}, triggered by ExchangeId: {}", throttleRate, newThrottle, exchange.getExchangeId());
274                        }
275                    }
276                    throttleRate = newThrottle;
277                }
278            }
279        }
280    }
281
282    @Override
283    protected void doStart() throws Exception {
284        if (isAsyncDelayed()) {
285            ObjectHelper.notNull(asyncExecutor, "executorService", this);
286        }
287        super.doStart();
288    }
289
290    @Override
291    protected void doShutdown() throws Exception {
292        if (shutdownAsyncExecutor && asyncExecutor != null) {
293            camelContext.getExecutorServiceManager().shutdownNow(asyncExecutor);
294        }
295        super.doShutdown();
296    }
297
298    /**
299     * Permit that implements the Delayed interface needed by DelayQueue.
300     */
301    private class ThrottlePermit implements Delayed {
302        private volatile long scheduledTime;
303
304        ThrottlePermit(final long delayMs) {
305            setDelayMs(delayMs);
306        }
307
308        public void setDelayMs(final long delayMs) {
309            this.scheduledTime = System.currentTimeMillis() + delayMs;
310        }
311
312        @Override
313        public long getDelay(final TimeUnit unit) {
314            return unit.convert(scheduledTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
315        }
316
317        @Override
318        public int compareTo(final Delayed o) {
319            return (int)(getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
320        }
321    }
322
323    public boolean isRejectExecution() {
324        return rejectExecution;
325    }
326
327    public void setRejectExecution(boolean rejectExecution) {
328        this.rejectExecution = rejectExecution;
329    }
330
331    public boolean isAsyncDelayed() {
332        return asyncDelayed;
333    }
334
335    public void setAsyncDelayed(boolean asyncDelayed) {
336        this.asyncDelayed = asyncDelayed;
337    }
338
339    public boolean isCallerRunsWhenRejected() {
340        return callerRunsWhenRejected;
341    }
342
343    public void setCallerRunsWhenRejected(boolean callerRunsWhenRejected) {
344        this.callerRunsWhenRejected = callerRunsWhenRejected;
345    }
346
347    public String getId() {
348        return id;
349    }
350
351    public void setId(final String id) {
352        this.id = id;
353    }
354
355    /**
356     * Sets the maximum number of requests per time period expression
357     */
358    public void setMaximumRequestsPerPeriodExpression(Expression maxRequestsPerPeriodExpression) {
359        this.maxRequestsPerPeriodExpression = maxRequestsPerPeriodExpression;
360    }
361
362    public Expression getMaximumRequestsPerPeriodExpression() {
363        return maxRequestsPerPeriodExpression;
364    }
365
366    /**
367     * Gets the current maximum request per period value.
368     */
369    public int getCurrentMaximumRequestsPerPeriod() {
370        return throttleRate;
371    }
372
373    /**
374     * Sets the time period during which the maximum number of requests apply
375     */
376    public void setTimePeriodMillis(final long timePeriodMillis) {
377        this.timePeriodMillis = timePeriodMillis;
378    }
379
380    public long getTimePeriodMillis() {
381        return timePeriodMillis;
382    }
383
384    public String getTraceLabel() {
385        return "throttle[" + maxRequestsPerPeriodExpression + " per: " + timePeriodMillis + "]";
386    }
387
388    @Override
389    public String toString() {
390        return "Throttler[requests: " + maxRequestsPerPeriodExpression + " per: " + timePeriodMillis + " (ms) to: "
391                + getProcessor() + "]";
392    }
393}