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.ScheduledExecutorService;
020import java.util.concurrent.atomic.AtomicLong;
021
022import org.apache.camel.AsyncCallback;
023import org.apache.camel.CamelContext;
024import org.apache.camel.Exchange;
025import org.apache.camel.Expression;
026import org.apache.camel.Processor;
027import org.apache.camel.RuntimeExchangeException;
028import org.apache.camel.Traceable;
029import org.apache.camel.spi.IdAware;
030import org.apache.camel.util.ObjectHelper;
031
032/**
033 * A <a href="http://camel.apache.org/throttler.html">Throttler</a>
034 * will set a limit on the maximum number of message exchanges which can be sent
035 * to a processor within a specific time period. <p/> This pattern can be
036 * extremely useful if you have some external system which meters access; such
037 * as only allowing 100 requests per second; or if huge load can cause a
038 * particular system to malfunction or to reduce its throughput you might want
039 * to introduce some throttling.
040 *
041 * @version
042 */
043public class Throttler extends DelayProcessorSupport implements Traceable, IdAware {
044    private String id;
045    private volatile long maximumRequestsPerPeriod;
046    private Expression maxRequestsPerPeriodExpression;
047    private AtomicLong timePeriodMillis = new AtomicLong(1000);
048    private volatile TimeSlot slot;
049    private boolean rejectExecution;
050
051    public Throttler(CamelContext camelContext, Processor processor, Expression maxRequestsPerPeriodExpression, long timePeriodMillis,
052                     ScheduledExecutorService executorService, boolean shutdownExecutorService, boolean rejectExecution) {
053        super(camelContext, processor, executorService, shutdownExecutorService);
054        this.rejectExecution = rejectExecution;
055
056        ObjectHelper.notNull(maxRequestsPerPeriodExpression, "maxRequestsPerPeriodExpression");
057        this.maxRequestsPerPeriodExpression = maxRequestsPerPeriodExpression;
058
059        if (timePeriodMillis <= 0) {
060            throw new IllegalArgumentException("TimePeriodMillis should be a positive number, was: " + timePeriodMillis);
061        }
062        this.timePeriodMillis.set(timePeriodMillis);
063    }
064
065    @Override
066    public String toString() {
067        return "Throttler[requests: " + maxRequestsPerPeriodExpression + " per: " + timePeriodMillis + " (ms) to: "
068               + getProcessor() + "]";
069    }
070
071    public String getTraceLabel() {
072        return "throttle[" + maxRequestsPerPeriodExpression + " per: " + timePeriodMillis + "]";
073    }
074
075    public String getId() {
076        return id;
077    }
078
079    public void setId(String id) {
080        this.id = id;
081    }
082
083    // Properties
084    // -----------------------------------------------------------------------
085
086    /**
087     * Sets the maximum number of requests per time period expression
088     */
089    public void setMaximumRequestsPerPeriodExpression(Expression maxRequestsPerPeriodExpression) {
090        this.maxRequestsPerPeriodExpression = maxRequestsPerPeriodExpression;
091    }
092
093    public Expression getMaximumRequestsPerPeriodExpression() {
094        return maxRequestsPerPeriodExpression;
095    }
096
097    public long getTimePeriodMillis() {
098        return timePeriodMillis.get();
099    }
100
101    /**
102     * Gets the current maximum request per period value.
103     */
104    public long getCurrentMaximumRequestsPerPeriod() {
105        return maximumRequestsPerPeriod;
106    }
107
108    /**
109     * Sets the time period during which the maximum number of requests apply
110     */
111    public void setTimePeriodMillis(long timePeriodMillis) {
112        this.timePeriodMillis.set(timePeriodMillis);
113    }
114
115    // Implementation methods
116    // -----------------------------------------------------------------------
117
118    protected long calculateDelay(Exchange exchange) {
119        // evaluate as Object first to see if we get any result at all
120        Object result = maxRequestsPerPeriodExpression.evaluate(exchange, Object.class);
121        if (maximumRequestsPerPeriod == 0 && result == null) {
122            throw new RuntimeExchangeException("The max requests per period expression was evaluated as null: " + maxRequestsPerPeriodExpression, exchange);
123        }
124
125        // then must convert value to long
126        Long longValue = exchange.getContext().getTypeConverter().convertTo(Long.class, result);
127        if (longValue != null) {
128            // log if we changed max period after initial setting
129            if (maximumRequestsPerPeriod > 0 && longValue.longValue() != maximumRequestsPerPeriod) {
130                log.debug("Throttler changed maximum requests per period from {} to {}", maximumRequestsPerPeriod, longValue);
131            }
132            if (maximumRequestsPerPeriod > longValue) {
133                slot.capacity = 0;
134            }
135            maximumRequestsPerPeriod = longValue;
136        }
137
138        if (maximumRequestsPerPeriod <= 0) {
139            throw new IllegalStateException("The maximumRequestsPerPeriod must be a positive number, was: " + maximumRequestsPerPeriod);
140        }
141
142        TimeSlot slot = nextSlot();
143        if (!slot.isActive()) {
144            long delay = slot.startTime - currentSystemTime();
145            return delay;
146        } else {
147            return 0;
148        }
149    }
150
151    /*
152     * Determine what the next available time slot is for handling an Exchange
153     */
154    protected synchronized TimeSlot nextSlot() throws ThrottlerRejectedExecutionException {
155        if (slot == null) {
156            slot = new TimeSlot();
157        } else {
158            if (rejectExecution && slot.isFull() && !slot.isPast()) {
159                throw new ThrottlerRejectedExecutionException("Exceed the max request limit!");
160            }
161            if (slot.isFull() || slot.isPast()) {
162                slot = slot.next();
163            }
164        }
165        slot.assign();
166        return slot;
167    }
168
169    /*
170    * A time slot is capable of handling a number of exchanges within a certain period of time.
171    */
172    protected class TimeSlot {
173
174        private volatile long capacity = Throttler.this.maximumRequestsPerPeriod;
175        private final long duration = Throttler.this.timePeriodMillis.get();
176        private final long startTime;
177
178        protected TimeSlot() {
179            this(System.currentTimeMillis());
180        }
181
182        protected TimeSlot(long startTime) {
183            this.startTime = startTime;
184        }
185
186        protected void assign() {
187            capacity--;
188        }
189
190        /*
191         * Start the next time slot either now or in the future
192         * (no time slots are being created in the past)
193         */
194        protected TimeSlot next() {
195            return new TimeSlot(Math.max(System.currentTimeMillis(), this.startTime + this.duration));
196        }
197
198        protected boolean isPast() {
199            long current = System.currentTimeMillis();
200            return current > (startTime + duration);
201        }
202
203        protected boolean isActive() {
204            long current = System.currentTimeMillis();
205            return startTime <= current && current < (startTime + duration);
206        }
207
208        protected boolean isFull() {
209            return capacity <= 0;
210        }
211    }
212
213    TimeSlot getSlot() {
214        return this.slot;
215    }
216
217    public boolean isRejectExecution() {
218        return rejectExecution;
219    }
220
221    public void setRejectExecution(boolean rejectExecution) {
222        this.rejectExecution = rejectExecution;
223    }
224    
225    @Override
226    protected boolean processDelay(Exchange exchange, AsyncCallback callback, long delay) {
227        if (isRejectExecution() && delay > 0) {
228            exchange.setException(new ThrottlerRejectedExecutionException("Exceed the max request limit!"));
229            callback.done(true);
230            return true;
231        } else {
232            return super.processDelay(exchange, callback, delay);
233        }
234    }
235}