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