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.loadbalancer;
018
019import java.util.List;
020import java.util.concurrent.RejectedExecutionException;
021import java.util.concurrent.atomic.AtomicInteger;
022
023import org.apache.camel.AsyncCallback;
024import org.apache.camel.AsyncProcessor;
025import org.apache.camel.CamelContext;
026import org.apache.camel.CamelContextAware;
027import org.apache.camel.Exchange;
028import org.apache.camel.Processor;
029import org.apache.camel.Traceable;
030import org.apache.camel.util.AsyncProcessorConverterHelper;
031import org.apache.camel.util.ExchangeHelper;
032import org.apache.camel.util.ObjectHelper;
033
034/**
035 * This FailOverLoadBalancer will failover to use next processor when an exception occurred
036 * <p/>
037 * This implementation mirrors the logic from the {@link org.apache.camel.processor.Pipeline} in the async variation
038 * as the failover load balancer is a specialized pipeline. So the trick is to keep doing the same as the
039 * pipeline to ensure it works the same and the async routing engine is flawless.
040 */
041public class FailOverLoadBalancer extends LoadBalancerSupport implements Traceable, CamelContextAware {
042
043    private final List<Class<?>> exceptions;
044    private CamelContext camelContext;
045    private boolean roundRobin;
046    private boolean sticky;
047    private int maximumFailoverAttempts = -1;
048
049    // stateful statistics
050    private final AtomicInteger counter = new AtomicInteger(-1);
051    private final AtomicInteger lastGoodIndex = new AtomicInteger(-1);
052    private final ExceptionFailureStatistics statistics = new ExceptionFailureStatistics();
053
054    public FailOverLoadBalancer() {
055        this.exceptions = null;
056    }
057
058    public FailOverLoadBalancer(List<Class<?>> exceptions) {
059        this.exceptions = exceptions;
060
061        // validate its all exception types
062        for (Class<?> type : exceptions) {
063            if (!ObjectHelper.isAssignableFrom(Throwable.class, type)) {
064                throw new IllegalArgumentException("Class is not an instance of Throwable: " + type);
065            }
066        }
067
068        statistics.init(exceptions);
069    }
070
071    @Override
072    public CamelContext getCamelContext() {
073        return camelContext;
074    }
075
076    @Override
077    public void setCamelContext(CamelContext camelContext) {
078        this.camelContext = camelContext;
079    }
080
081    public int getLastGoodIndex() {
082        return lastGoodIndex.get();
083    }
084
085    public List<Class<?>> getExceptions() {
086        return exceptions;
087    }
088
089    public boolean isRoundRobin() {
090        return roundRobin;
091    }
092
093    public void setRoundRobin(boolean roundRobin) {
094        this.roundRobin = roundRobin;
095    }
096
097    public boolean isSticky() {
098        return sticky;
099    }
100
101    public void setSticky(boolean sticky) {
102        this.sticky = sticky;
103    }
104
105    public int getMaximumFailoverAttempts() {
106        return maximumFailoverAttempts;
107    }
108
109    public void setMaximumFailoverAttempts(int maximumFailoverAttempts) {
110        this.maximumFailoverAttempts = maximumFailoverAttempts;
111    }
112
113    /**
114     * Should the given failed Exchange failover?
115     *
116     * @param exchange the exchange that failed
117     * @return <tt>true</tt> to failover
118     */
119    protected boolean shouldFailOver(Exchange exchange) {
120        if (exchange == null) {
121            return false;
122        }
123
124        boolean answer = false;
125
126        if (exchange.getException() != null) {
127            if (exceptions == null || exceptions.isEmpty()) {
128                // always failover if no exceptions defined
129                answer = true;
130            } else {
131                for (Class<?> exception : exceptions) {
132                    // will look in exception hierarchy
133                    if (exchange.getException(exception) != null) {
134                        answer = true;
135                        break;
136                    }
137                }
138            }
139
140            if (answer) {
141                // record the failure in the statistics
142                statistics.onHandledFailure(exchange.getException());
143            }
144        }
145
146        log.trace("Should failover: {} for exchangeId: {}", answer, exchange.getExchangeId());
147
148        return answer;
149    }
150
151    @Override
152    public boolean isRunAllowed() {
153        // determine if we can still run, or the camel context is forcing a shutdown
154        boolean forceShutdown = camelContext.getShutdownStrategy().forceShutdown(this);
155        if (forceShutdown) {
156            log.trace("Run not allowed as ShutdownStrategy is forcing shutting down");
157        }
158        return !forceShutdown && super.isRunAllowed();
159    }
160
161    public boolean process(final Exchange exchange, final AsyncCallback callback) {
162        final List<Processor> processors = getProcessors();
163
164        final AtomicInteger index = new AtomicInteger();
165        final AtomicInteger attempts = new AtomicInteger();
166        boolean first = true;
167        // use a copy of the original exchange before failover to avoid populating side effects
168        // directly into the original exchange
169        Exchange copy = null;
170
171        // get the next processor
172        if (isSticky()) {
173            int idx = lastGoodIndex.get();
174            if (idx == -1) {
175                idx = 0;
176            }
177            index.set(idx);
178        } else if (isRoundRobin()) {
179            if (counter.incrementAndGet() >= processors.size()) {
180                counter.set(0);
181            }
182            index.set(counter.get());
183        }
184        log.trace("Failover starting with endpoint index {}", index);
185
186        while (first || shouldFailOver(copy)) {
187
188            // can we still run
189            if (!isRunAllowed()) {
190                log.trace("Run not allowed, will reject executing exchange: {}", exchange);
191                if (exchange.getException() == null) {
192                    exchange.setException(new RejectedExecutionException());
193                }
194                // we cannot process so invoke callback
195                callback.done(true);
196                return true;
197            }
198
199            if (!first) {
200                attempts.incrementAndGet();
201                // are we exhausted by attempts?
202                if (maximumFailoverAttempts > -1 && attempts.get() > maximumFailoverAttempts) {
203                    log.debug("Breaking out of failover after {} failover attempts", attempts);
204                    break;
205                }
206
207                index.incrementAndGet();
208                counter.incrementAndGet();
209            } else {
210                // flip first switch
211                first = false;
212            }
213
214            if (index.get() >= processors.size()) {
215                // out of bounds
216                if (isRoundRobin()) {
217                    log.trace("Failover is round robin enabled and therefore starting from the first endpoint");
218                    index.set(0);
219                    counter.set(0);
220                } else {
221                    // no more processors to try
222                    log.trace("Breaking out of failover as we reached the end of endpoints to use for failover");
223                    break;
224                }
225            }
226
227            // try again but copy original exchange before we failover
228            copy = prepareExchangeForFailover(exchange);
229            Processor processor = processors.get(index.get());
230
231            // process the exchange
232            boolean sync = processExchange(processor, exchange, copy, attempts, index, callback, processors);
233
234            // continue as long its being processed synchronously
235            if (!sync) {
236                log.trace("Processing exchangeId: {} is continued being processed asynchronously", exchange.getExchangeId());
237                // the remainder of the failover will be completed async
238                // so we break out now, then the callback will be invoked which then continue routing from where we left here
239                return false;
240            }
241
242            log.trace("Processing exchangeId: {} is continued being processed synchronously", exchange.getExchangeId());
243        }
244
245        // remember last good index
246        lastGoodIndex.set(index.get());
247
248        // and copy the current result to original so it will contain this result of this eip
249        if (copy != null) {
250            ExchangeHelper.copyResults(exchange, copy);
251        }
252        log.debug("Failover complete for exchangeId: {} >>> {}", exchange.getExchangeId(), exchange);
253        callback.done(true);
254        return true;
255    }
256
257    /**
258     * Prepares the exchange for failover
259     *
260     * @param exchange the exchange
261     * @return a copy of the exchange to use for failover
262     */
263    protected Exchange prepareExchangeForFailover(Exchange exchange) {
264        // use a copy of the exchange to avoid side effects on the original exchange
265        return ExchangeHelper.createCopy(exchange, true);
266    }
267
268    private boolean processExchange(Processor processor, Exchange exchange, Exchange copy,
269                                    AtomicInteger attempts, AtomicInteger index,
270                                    AsyncCallback callback, List<Processor> processors) {
271        if (processor == null) {
272            throw new IllegalStateException("No processors could be chosen to process " + copy);
273        }
274        log.debug("Processing failover at attempt {} for {}", attempts, copy);
275
276        AsyncProcessor albp = AsyncProcessorConverterHelper.convert(processor);
277        return albp.process(copy, new FailOverAsyncCallback(exchange, copy, attempts, index, callback, processors));
278    }
279
280    /**
281     * Failover logic to be executed asynchronously if one of the failover endpoints
282     * is a real {@link AsyncProcessor}.
283     */
284    private final class FailOverAsyncCallback implements AsyncCallback {
285
286        private final Exchange exchange;
287        private Exchange copy;
288        private final AtomicInteger attempts;
289        private final AtomicInteger index;
290        private final AsyncCallback callback;
291        private final List<Processor> processors;
292
293        private FailOverAsyncCallback(Exchange exchange, Exchange copy, AtomicInteger attempts, AtomicInteger index, AsyncCallback callback, List<Processor> processors) {
294            this.exchange = exchange;
295            this.copy = copy;
296            this.attempts = attempts;
297            this.index = index;
298            this.callback = callback;
299            this.processors = processors;
300        }
301
302        public void done(boolean doneSync) {
303            // we only have to handle async completion of the pipeline
304            if (doneSync) {
305                return;
306            }
307
308            while (shouldFailOver(copy)) {
309
310                // can we still run
311                if (!isRunAllowed()) {
312                    log.trace("Run not allowed, will reject executing exchange: {}", exchange);
313                    if (exchange.getException() == null) {
314                        exchange.setException(new RejectedExecutionException());
315                    }
316                    // we cannot process so invoke callback
317                    callback.done(false);
318                }
319
320                attempts.incrementAndGet();
321                // are we exhausted by attempts?
322                if (maximumFailoverAttempts > -1 && attempts.get() > maximumFailoverAttempts) {
323                    log.trace("Breaking out of failover after {} failover attempts", attempts);
324                    break;
325                }
326
327                index.incrementAndGet();
328                counter.incrementAndGet();
329
330                if (index.get() >= processors.size()) {
331                    // out of bounds
332                    if (isRoundRobin()) {
333                        log.trace("Failover is round robin enabled and therefore starting from the first endpoint");
334                        index.set(0);
335                        counter.set(0);
336                    } else {
337                        // no more processors to try
338                        log.trace("Breaking out of failover as we reached the end of endpoints to use for failover");
339                        break;
340                    }
341                }
342
343                // try again but prepare exchange before we failover
344                copy = prepareExchangeForFailover(exchange);
345                Processor processor = processors.get(index.get());
346
347                // try to failover using the next processor
348                doneSync = processExchange(processor, exchange, copy, attempts, index, callback, processors);
349                if (!doneSync) {
350                    log.trace("Processing exchangeId: {} is continued being processed asynchronously", exchange.getExchangeId());
351                    // the remainder of the failover will be completed async
352                    // so we break out now, then the callback will be invoked which then continue routing from where we left here
353                    return;
354                }
355            }
356
357            // remember last good index
358            lastGoodIndex.set(index.get());
359
360            // and copy the current result to original so it will contain this result of this eip
361            if (copy != null) {
362                ExchangeHelper.copyResults(exchange, copy);
363            }
364            log.debug("Failover complete for exchangeId: {} >>> {}", exchange.getExchangeId(), exchange);
365            // signal callback we are done
366            callback.done(false);
367        }
368    }
369
370    public String toString() {
371        return "FailoverLoadBalancer[" + getProcessors() + "]";
372    }
373
374    public String getTraceLabel() {
375        return "failover";
376    }
377
378    public ExceptionFailureStatistics getExceptionFailureStatistics() {
379        return statistics;
380    }
381
382    public void reset() {
383        // reset state
384        lastGoodIndex.set(-1);
385        counter.set(-1);
386        statistics.reset();
387    }
388
389    @Override
390    protected void doStart() throws Exception {
391        super.doStart();
392
393        // reset state
394        reset();
395    }
396
397    @Override
398    protected void doStop() throws Exception {
399        super.doStop();
400        // noop
401    }
402
403}