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}