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}