diff --git a/hystrix-contrib/hystrix-javanica/build.gradle b/hystrix-contrib/hystrix-javanica/build.gradle index 7fa6d5de7..dea764711 100644 --- a/hystrix-contrib/hystrix-javanica/build.gradle +++ b/hystrix-contrib/hystrix-javanica/build.gradle @@ -5,22 +5,24 @@ apply plugin: 'idea' aspectjVersion = '1.7.4' dependencies { - compile project(':hystrix-core') + compile project(':hystrix-core') - compile "org.aspectj:aspectjweaver:$aspectjVersion" - compile "org.aspectj:aspectjrt:$aspectjVersion" + compile "org.aspectj:aspectjweaver:$aspectjVersion" + compile "org.aspectj:aspectjrt:$aspectjVersion" - compile 'com.google.guava:guava:15.0' - compile 'commons-collections:commons-collections:3.2.1' - compile 'org.apache.commons:commons-lang3:3.1' - compile 'com.google.code.findbugs:jsr305:2.0.0' - testCompile 'junit:junit-dep:4.10' - testCompile 'org.springframework:spring-core:4.0.0.RELEASE' - testCompile 'org.springframework:spring-context:4.0.0.RELEASE' - testCompile 'org.springframework:spring-aop:4.0.0.RELEASE' - testCompile 'org.springframework:spring-test:4.0.0.RELEASE' - testCompile 'cglib:cglib:3.1' - testCompile 'org.mockito:mockito-all:1.9.5' + compile 'com.google.guava:guava:15.0' + compile 'commons-collections:commons-collections:3.2.1' + compile 'org.apache.commons:commons-lang3:3.1' + compile 'com.google.code.findbugs:jsr305:2.0.0' + testCompile 'junit:junit-dep:4.10' + testCompile 'org.springframework:spring-core:4.0.0.RELEASE' + testCompile 'org.springframework:spring-context:4.0.0.RELEASE' + testCompile 'org.springframework:spring-aop:4.0.0.RELEASE' + testCompile 'org.springframework:spring-test:4.0.0.RELEASE' + testCompile 'cglib:cglib:3.1' + testCompile 'org.mockito:mockito-all:1.9.5' + testCompile 'log4j:log4j:1.2.17' + testCompile 'org.slf4j:slf4j-log4j12:1.7.7' } eclipse { diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/collapser/CommandCollapser.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/collapser/CommandCollapser.java index c37a9fb98..c51b949a7 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/collapser/CommandCollapser.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/collapser/CommandCollapser.java @@ -15,6 +15,7 @@ */ package com.netflix.hystrix.contrib.javanica.collapser; +import com.google.common.base.Optional; import com.netflix.hystrix.HystrixCollapser; import com.netflix.hystrix.HystrixCollapserKey; import com.netflix.hystrix.HystrixCommand; @@ -33,7 +34,7 @@ * Collapses multiple requests into a single {@link HystrixCommand} execution based * on a time window and optionally a max batch size. */ -public class CommandCollapser extends HystrixCollapser, Object, Object> { +public class CommandCollapser extends HystrixCollapser>, Object, Object> { private MetaHolder metaHolder; @@ -69,7 +70,7 @@ public Object getRequestArgument() { * Creates batch command. */ @Override - protected HystrixCommand> createCommand( + protected HystrixCommand>> createCommand( Collection> collapsedRequests) { BatchHystrixCommand command = BatchHystrixCommandFactory.getInstance().create(metaHolder, collapsedRequests); command.setFallbackEnabled(metaHolder.getHystrixCollapser().fallbackEnabled()); @@ -80,14 +81,17 @@ protected HystrixCommand> createCommand( * {@inheritDoc} */ @Override - protected void mapResponseToRequests(List batchResponse, + protected void mapResponseToRequests(List> batchResponse, Collection> collapsedRequests) { if (batchResponse.size() < collapsedRequests.size()) { throw new RuntimeException(createMessage(collapsedRequests, batchResponse)); } int count = 0; for (CollapsedRequest request : collapsedRequests) { - request.setResponse(batchResponse.get(count++)); + Optional response = batchResponse.get(count++); + if (response.isPresent()) { // allows prevent IllegalStateException + request.setResponse(response.get()); + } } } @@ -116,7 +120,7 @@ public Setter build() { } private String createMessage(Collection> requests, - List response) { + List> response) { return ERROR_MSG + arrayFormat(ERROR_MSF_TEMPLATE, new Object[]{getCollapserKey().name(), requests.size(), response.size()}).getMessage(); } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/AbstractHystrixCommand.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/AbstractHystrixCommand.java index 002daf696..111d71c33 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/AbstractHystrixCommand.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/AbstractHystrixCommand.java @@ -19,7 +19,9 @@ import com.google.common.base.Throwables; import com.netflix.hystrix.HystrixCollapser; import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager; +import com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException; import com.netflix.hystrix.exception.HystrixBadRequestException; +import com.netflix.hystrix.exception.HystrixRuntimeException; import javax.annotation.concurrent.ThreadSafe; import java.util.Collection; @@ -136,12 +138,18 @@ protected String getCacheKey() { return key; } + /** + * Check whether triggered exception is ignorable. + * + * @param throwable the exception occurred during a command execution + * @return true if exception is ignorable, otherwise - false + */ boolean isIgnorable(Throwable throwable) { if (ignoreExceptions == null || ignoreExceptions.length == 0) { return false; } for (Class ignoreException : ignoreExceptions) { - if (throwable.getClass().isAssignableFrom(ignoreException)) { + if (ignoreException.isAssignableFrom(throwable.getClass())) { return true; } } @@ -150,20 +158,28 @@ boolean isIgnorable(Throwable throwable) { /** * Executes an action. If an action has failed and an exception is ignorable then propagate it as HystrixBadRequestException - * otherwise propagate it as RuntimeException to trigger fallback method. + * otherwise propagate original exception to trigger fallback method. + * Note: If an exception occurred in a command directly extends {@link java.lang.Throwable} then this exception cannot be re-thrown + * as original exception because HystrixCommand.run() allows throw subclasses of {@link java.lang.Exception}. + * Thus we need to wrap cause in RuntimeException, anyway in this case the fallback logic will be triggered. * * @param action the action * @return result of command action execution */ - Object process(Action action) throws RuntimeException { + Object process(Action action) throws Exception { Object result; try { result = action.execute(); - } catch (Throwable throwable) { - if (isIgnorable(throwable)) { - throw new HystrixBadRequestException(throwable.getMessage(), throwable); + } catch (CommandActionExecutionException throwable) { + Throwable cause = throwable.getCause(); + if (isIgnorable(cause)) { + throw new HystrixBadRequestException(cause.getMessage(), cause); + } + if (cause instanceof Exception) { + throw (Exception) cause; + } else { + throw Throwables.propagate(cause); } - throw Throwables.propagate(throwable); } return result; } @@ -186,7 +202,54 @@ protected T getFallback() { * Common action. */ abstract class Action { - abstract Object execute(); + /** + * Each implementation of this method should wrap any exceptions in CommandActionExecutionException. + * + * @return execution result + * @throws CommandActionExecutionException + */ + abstract Object execute() throws CommandActionExecutionException; + } + + + /** + * Builder to create error message for failed fallback operation. + */ + static class FallbackErrorMessageBuilder { + private StringBuilder builder = new StringBuilder("failed to processed fallback"); + + static FallbackErrorMessageBuilder create() { + return new FallbackErrorMessageBuilder(); + } + + public FallbackErrorMessageBuilder append(CommandAction action, Throwable throwable) { + return commandAction(action).exception(throwable); + } + + private FallbackErrorMessageBuilder commandAction(CommandAction action) { + if (action instanceof CommandExecutionAction || action instanceof LazyCommandExecutionAction) { + builder.append(": '").append(action.getActionName()).append("'. ") + .append(action.getActionName()).append(" fallback is a hystrix command. "); + } else if (action instanceof MethodExecutionAction) { + builder.append(" is the method: '").append(action.getActionName()).append("'. "); + } + return this; + } + + private FallbackErrorMessageBuilder exception(Throwable throwable) { + if (throwable instanceof HystrixBadRequestException) { + builder.append("exception: '").append(throwable.getCause().getClass()) + .append("' occurred in fallback was ignored and wrapped to HystrixBadRequestException.\n"); + } else if (throwable instanceof HystrixRuntimeException) { + builder.append("exception: '").append(throwable.getCause().getClass()) + .append("' occurred in fallback wasn't ignored.\n"); + } + return this; + } + + public String build() { + return builder.toString(); + } } } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/BatchHystrixCommand.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/BatchHystrixCommand.java index 9a22638e9..9a1e5089e 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/BatchHystrixCommand.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/BatchHystrixCommand.java @@ -16,9 +16,13 @@ package com.netflix.hystrix.contrib.javanica.command; +import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.netflix.hystrix.HystrixCollapser; +import com.netflix.hystrix.contrib.javanica.exception.FallbackInvocationException; import com.netflix.hystrix.exception.HystrixBadRequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.concurrent.ThreadSafe; import java.util.Collection; @@ -29,7 +33,9 @@ * This command is used in collapser. */ @ThreadSafe -public class BatchHystrixCommand extends AbstractHystrixCommand> { +public class BatchHystrixCommand extends AbstractHystrixCommand>> { + + private static final Logger LOGGER = LoggerFactory.getLogger(GenericCommand.class); /** * If some error occurs during the processing in run() then if {@link #fallbackEnabled} is true then the {@link #processWithFallback} @@ -63,22 +69,21 @@ public void setFallbackEnabled(boolean fallbackEnabled) { * {@inheritDoc} */ @Override - protected List run() throws Exception { - List response = Lists.newArrayList(); + protected List> run() throws Exception { + List> response = Lists.newArrayList(); for (HystrixCollapser.CollapsedRequest request : getCollapsedRequests()) { final Object[] args = (Object[]) request.getArgument(); try { - response.add(fallbackEnabled ? processWithFallback(args) : process(args)); - } catch (RuntimeException ex) { + response.add(Optional.of(fallbackEnabled ? processWithFallback(args) : process(args))); + } catch (Exception ex) { request.setException(ex); - response.add(null); + response.add(Optional.absent()); } - } return response; } - private Object process(final Object[] args) { + private Object process(final Object[] args) throws Exception { return process(new Action() { @Override Object execute() { @@ -87,16 +92,20 @@ Object execute() { }); } - private Object processWithFallback(final Object[] args) { - Object result = null; + private Object processWithFallback(final Object[] args) throws Exception { + Object result; try { result = process(args); - } catch (RuntimeException ex) { - if (ex instanceof HystrixBadRequestException || getFallbackAction() == null) { + } catch (Exception ex) { + if (ex instanceof HystrixBadRequestException) { throw ex; } else { if (getFallbackAction() != null) { result = processFallback(args); + } else { + // if command doesn't have fallback then + // call super.getFallback() that throws exception by default. + result = super.getFallback(); } } } @@ -104,12 +113,23 @@ private Object processWithFallback(final Object[] args) { } private Object processFallback(final Object[] args) { - return process(new Action() { - @Override - Object execute() { - return getFallbackAction().executeWithArgs(ExecutionType.SYNCHRONOUS, args); + if (getFallbackAction() != null) { + final CommandAction commandAction = getFallbackAction(); + try { + return process(new Action() { + @Override + Object execute() { + return commandAction.executeWithArgs(ExecutionType.SYNCHRONOUS, args); + } + }); + } catch (Throwable e) { + LOGGER.error(FallbackErrorMessageBuilder.create() + .append(commandAction, e).build()); + throw new FallbackInvocationException(e.getCause()); } - }); + } else { + return super.getFallback(); + } } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandAction.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandAction.java index 5b90249fa..5c11792b5 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandAction.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandAction.java @@ -15,13 +15,37 @@ */ package com.netflix.hystrix.contrib.javanica.command; +import com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException; + /** * Simple action to encapsulate some logic to process it in a Hystrix command. */ public abstract class CommandAction { - public abstract Object execute(ExecutionType executionType); + /** + * Executes action in accordance with the given execution type. + * + * @param executionType the execution type + * @return result of execution + * @throws com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException + */ + public abstract Object execute(ExecutionType executionType) throws CommandActionExecutionException; + + /** + * Executes action with parameters in accordance with the given execution ty + * + * @param executionType the execution type + * @param args the parameters of the action + * @return result of execution + * @throws com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException + */ + public abstract Object executeWithArgs(ExecutionType executionType, Object[] args) throws CommandActionExecutionException; - public abstract Object executeWithArgs(ExecutionType executionType, Object[] args); + /** + * Gets action name. Useful for debugging. + * + * @return the action name + */ + public abstract String getActionName(); } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandExecutionAction.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandExecutionAction.java index 658ed89b9..c32bfc388 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandExecutionAction.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/CommandExecutionAction.java @@ -15,6 +15,8 @@ */ package com.netflix.hystrix.contrib.javanica.command; +import com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException; + /** * Action to execute a Hystrix command. */ @@ -31,14 +33,29 @@ public CommandExecutionAction(AbstractHystrixCommand hystrixCommand) { this.hystrixCommand = hystrixCommand; } + public AbstractHystrixCommand getHystrixCommand() { + return hystrixCommand; + } + @Override - public Object execute(ExecutionType executionType) { + public Object execute(ExecutionType executionType) throws CommandActionExecutionException { return CommandExecutor.execute(hystrixCommand, executionType); } @Override - public Object executeWithArgs(ExecutionType executionType, Object[] args) { + public Object executeWithArgs(ExecutionType executionType, Object[] args) throws CommandActionExecutionException { return CommandExecutor.execute(hystrixCommand, executionType); } + /** + * {@inheritDoc} + */ + @Override + public String getActionName() { + if (hystrixCommand != null) { + return hystrixCommand.getCommandKey().name(); + } + return ""; + } + } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/GenericCommand.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/GenericCommand.java index ac8432948..1682ee749 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/GenericCommand.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/GenericCommand.java @@ -16,6 +16,9 @@ package com.netflix.hystrix.contrib.javanica.command; import com.netflix.hystrix.HystrixCollapser; +import com.netflix.hystrix.contrib.javanica.exception.FallbackInvocationException; +import com.netflix.hystrix.exception.HystrixBadRequestException; +import com.netflix.hystrix.exception.HystrixRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,19 +67,29 @@ Object execute() { * HystrixCommand annotation, otherwise current implementation throws RuntimeException and leaves the caller to deal with it * (see {@link super#getFallback()}). * The getFallback() is always processed synchronously. + * Since getFallback() can throw only runtime exceptions thus any exceptions are thrown within getFallback() method + * are wrapped in {@link FallbackInvocationException}. + * A caller gets {@link com.netflix.hystrix.exception.HystrixRuntimeException} + * and should call getCause to get original exception that was thrown in getFallback(). * * @return result of invocation of fallback method or RuntimeException */ @Override protected Object getFallback() { if (getFallbackAction() != null) { - return process(new Action() { - @Override - Object execute() { - return getFallbackAction().execute(ExecutionType.SYNCHRONOUS); - } - }); - + final CommandAction commandAction = getFallbackAction(); + try { + return process(new Action() { + @Override + Object execute() { + return commandAction.execute(ExecutionType.SYNCHRONOUS); + } + }); + } catch (Throwable e) { + LOGGER.error(FallbackErrorMessageBuilder.create() + .append(commandAction, e).build()); + throw new FallbackInvocationException(e.getCause()); + } } else { return super.getFallback(); } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/LazyCommandExecutionAction.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/LazyCommandExecutionAction.java index 5dc8ebbf8..ffe55179b 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/LazyCommandExecutionAction.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/LazyCommandExecutionAction.java @@ -17,9 +17,14 @@ import com.netflix.hystrix.HystrixCollapser; +import com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException; +import org.apache.commons.lang3.StringUtils; import java.util.Collection; +/** + * This action creates related hystrix commands on demand when command creation can be postponed. + */ public class LazyCommandExecutionAction extends CommandAction { private MetaHolder metaHolder; @@ -36,18 +41,34 @@ public LazyCommandExecutionAction(HystrixCommandFactory commandFactory, this.collapsedRequests = collapsedRequests; } + /** + * {@inheritDoc} + */ @Override - public Object execute(ExecutionType executionType) { + public Object execute(ExecutionType executionType) throws CommandActionExecutionException { AbstractHystrixCommand abstractHystrixCommand = commandFactory.create(createHolder(executionType), collapsedRequests); return new CommandExecutionAction(abstractHystrixCommand).execute(executionType); } + /** + * {@inheritDoc} + */ @Override - public Object executeWithArgs(ExecutionType executionType, Object[] args) { + public Object executeWithArgs(ExecutionType executionType, Object[] args) throws CommandActionExecutionException { AbstractHystrixCommand abstractHystrixCommand = commandFactory.create(createHolder(executionType, args), collapsedRequests); return new CommandExecutionAction(abstractHystrixCommand).execute(executionType); } + /** + * {@inheritDoc} + */ + @Override + public String getActionName() { + return StringUtils.isNotEmpty(metaHolder.getHystrixCommand().commandKey()) ? + metaHolder.getHystrixCommand().commandKey() + : metaHolder.getDefaultCommandKey(); + } + private MetaHolder createHolder(ExecutionType executionType) { return MetaHolder.builder() .obj(metaHolder.getObj()) diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/MethodExecutionAction.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/MethodExecutionAction.java index 466a0dddc..448c9c406 100644 --- a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/MethodExecutionAction.java +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/command/MethodExecutionAction.java @@ -1,13 +1,34 @@ +/** + * Copyright 2012 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.netflix.hystrix.contrib.javanica.command; -import com.google.common.base.Throwables; import com.netflix.hystrix.contrib.javanica.command.closure.Closure; import com.netflix.hystrix.contrib.javanica.command.closure.ClosureFactoryRegistry; +import com.netflix.hystrix.contrib.javanica.exception.CommandActionExecutionException; +import com.netflix.hystrix.contrib.javanica.exception.ExceptionUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +/** + * This implementation invokes methods using java reflection. + * If {@link Method#invoke(Object, Object...)} throws exception then this exception is wrapped to {@link CommandActionExecutionException} + * for further unwrapping and processing. + */ public class MethodExecutionAction extends CommandAction { private static final Object[] EMPTY_ARGS = new Object[]{}; @@ -41,7 +62,7 @@ public Object[] getArgs() { } @Override - public Object execute(ExecutionType executionType) { + public Object execute(ExecutionType executionType) throws CommandActionExecutionException { return executeWithArgs(executionType, _args); } @@ -51,7 +72,7 @@ public Object execute(ExecutionType executionType) { * @return result of execution */ @Override - public Object executeWithArgs(ExecutionType executionType, Object[] args) { + public Object executeWithArgs(ExecutionType executionType, Object[] args) throws CommandActionExecutionException { if (ExecutionType.SYNCHRONOUS.equals(executionType)) { return execute(object, method, args); } else { @@ -60,12 +81,20 @@ public Object executeWithArgs(ExecutionType executionType, Object[] args) { } } + /** + * {@inheritDoc} + */ + @Override + public String getActionName() { + return method.getName(); + } + /** * Invokes the method. * * @return result of execution */ - private Object execute(Object o, Method m, Object... args) { + private Object execute(Object o, Method m, Object... args) throws CommandActionExecutionException { Object result = null; try { m.setAccessible(true); // suppress Java language access @@ -78,8 +107,13 @@ private Object execute(Object o, Method m, Object... args) { return result; } - private void propagateCause(Throwable throwable) { - throw Throwables.propagate(throwable.getCause()); + /** + * Retrieves cause exception and wraps to {@link CommandActionExecutionException}. + * + * @param throwable the throwable + */ + private void propagateCause(Throwable throwable) throws CommandActionExecutionException { + ExceptionUtils.propagateCause(throwable); } } diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/CommandActionExecutionException.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/CommandActionExecutionException.java new file mode 100644 index 000000000..11d60f3c6 --- /dev/null +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/CommandActionExecutionException.java @@ -0,0 +1,37 @@ +/** + * Copyright 2012 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.hystrix.contrib.javanica.exception; + +/** + * This exception is used to wrap original exceptions to push through javanica infrastructure. + */ +public class CommandActionExecutionException extends RuntimeException { + + /** + * Creates exceptions instance with cause is original exception. + * + * @param cause the original exception + */ + public CommandActionExecutionException(Throwable cause) { + super(cause); + } + + /** + * Default constructor. + */ + public CommandActionExecutionException() { + } +} diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/ExceptionUtils.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/ExceptionUtils.java new file mode 100644 index 000000000..65bb9c764 --- /dev/null +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/ExceptionUtils.java @@ -0,0 +1,40 @@ +/** + * Copyright 2012 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.hystrix.contrib.javanica.exception; + +/** + * Util class to work with exceptions. + */ +public class ExceptionUtils { + + /** + * Retrieves cause exception and wraps to {@link CommandActionExecutionException}. + * + * @param throwable the throwable + */ + public static void propagateCause(Throwable throwable) throws CommandActionExecutionException { + throw new CommandActionExecutionException(throwable.getCause()); + } + + /** + * Wraps cause exception to {@link CommandActionExecutionException}. + * + * @param throwable the throwable + */ + public static CommandActionExecutionException wrapCause(Throwable throwable) { + return new CommandActionExecutionException(throwable.getCause()); + } +} diff --git a/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/FallbackInvocationException.java b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/FallbackInvocationException.java new file mode 100644 index 000000000..7149dcdad --- /dev/null +++ b/hystrix-contrib/hystrix-javanica/src/main/java/com/netflix/hystrix/contrib/javanica/exception/FallbackInvocationException.java @@ -0,0 +1,33 @@ +/** + * Copyright 2012 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.hystrix.contrib.javanica.exception; + +/** + * Exception specifies error occurred in fallback method. + */ +public class FallbackInvocationException extends RuntimeException { + + public FallbackInvocationException() { + } + + public FallbackInvocationException(String message, Throwable cause) { + super(message, cause); + } + + public FallbackInvocationException(Throwable cause) { + super(cause); + } +} diff --git a/hystrix-contrib/hystrix-javanica/src/test/java/com/netflix/hystrix/contrib/javanica/test/spring/error/ErrorPropagationTest.java b/hystrix-contrib/hystrix-javanica/src/test/java/com/netflix/hystrix/contrib/javanica/test/spring/error/ErrorPropagationTest.java index 67cd8c912..22ed89e72 100644 --- a/hystrix-contrib/hystrix-javanica/src/test/java/com/netflix/hystrix/contrib/javanica/test/spring/error/ErrorPropagationTest.java +++ b/hystrix-contrib/hystrix-javanica/src/test/java/com/netflix/hystrix/contrib/javanica/test/spring/error/ErrorPropagationTest.java @@ -6,6 +6,7 @@ import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.test.spring.conf.AopCglibConfig; import com.netflix.hystrix.contrib.javanica.test.spring.domain.User; +import com.netflix.hystrix.exception.HystrixRuntimeException; import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; import org.junit.Before; import org.junit.Test; @@ -17,11 +18,12 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import java.util.HashMap; +import java.util.Map; + import static com.netflix.hystrix.contrib.javanica.CommonUtils.getHystrixCommandByKey; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * Test covers "Error Propagation" functionality. @@ -33,6 +35,15 @@ public class ErrorPropagationTest { private static final String COMMAND_KEY = "getUserById"; + private static final Map USERS; + + static { + USERS = new HashMap(); + USERS.put("1", new User("1", "user_1")); + USERS.put("2", new User("2", "user_2")); + USERS.put("3", new User("3", "user_3")); + } + @Autowired private UserService userService; @@ -45,11 +56,12 @@ public void setUp() throws Exception { userService.setFailoverService(failoverService); } - @Test(expected = IllegalArgumentException.class) - public void testGetUserByEmptyId() { + @Test(expected = BadRequestException.class) + public void testGetUserByBadId() throws NotFoundException { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { - userService.getUserById(""); + String badId = ""; + userService.getUserById(badId); } finally { assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); com.netflix.hystrix.HystrixCommand getUserCommand = getHystrixCommandByKey(COMMAND_KEY); @@ -61,11 +73,11 @@ public void testGetUserByEmptyId() { } } - @Test(expected = NullPointerException.class) - public void testGetUserByNullId() { + @Test(expected = NotFoundException.class) + public void testGetNonExistentUser() throws NotFoundException { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { - userService.getUserById(null); + userService.getUserById("4"); // user with id 4 doesn't exist } finally { assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); com.netflix.hystrix.HystrixCommand getUserCommand = getHystrixCommandByKey(COMMAND_KEY); @@ -77,6 +89,38 @@ public void testGetUserByNullId() { } } + @Test // don't expect any exceptions because fallback must be triggered + public void testActivateUser() throws NotFoundException, ActivationException { + HystrixRequestContext context = HystrixRequestContext.initializeContext(); + try { + userService.activateUser("1"); // this method always throws ActivationException + } finally { + assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); + com.netflix.hystrix.HystrixCommand activateUserCommand = getHystrixCommandByKey("activateUser"); + // will not affect metrics + assertTrue(activateUserCommand.getExecutionEvents().contains(HystrixEventType.FAILURE)); + assertTrue(activateUserCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS)); + // and will not trigger fallback logic + verify(failoverService, atLeastOnce()).activate(); + context.shutdown(); + } + } + + @Test(expected = HystrixRuntimeException.class) + public void testBlockUser() throws NotFoundException, ActivationException, OperationException { + HystrixRequestContext context = HystrixRequestContext.initializeContext(); + try { + userService.blockUser("1"); // this method always throws ActivationException + } finally { + assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); + com.netflix.hystrix.HystrixCommand activateUserCommand = getHystrixCommandByKey("blockUser"); + // will not affect metrics + assertTrue(activateUserCommand.getExecutionEvents().contains(HystrixEventType.FAILURE)); + assertTrue(activateUserCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_FAILURE)); + context.shutdown(); + } + } + public static class UserService { private FailoverService failoverService; @@ -85,22 +129,62 @@ public void setFailoverService(FailoverService failoverService) { this.failoverService = failoverService; } - @HystrixCommand(commandKey = COMMAND_KEY, ignoreExceptions = {NullPointerException.class, IllegalArgumentException.class}, + @HystrixCommand( + commandKey = COMMAND_KEY, + ignoreExceptions = { + BadRequestException.class, + NotFoundException.class + }, fallbackMethod = "fallback") - public User getUserById(String id) { + public User getUserById(String id) throws NotFoundException { validate(id); - return new User(id, "name" + id); // it should be network call + if (!USERS.containsKey(id)) { + throw new NotFoundException("user with id: " + id + " not found"); + } + return USERS.get(id); + } + + + @HystrixCommand( + ignoreExceptions = {BadRequestException.class, NotFoundException.class}, + fallbackMethod = "activateFallback") + public void activateUser(String id) throws NotFoundException, ActivationException { + validate(id); + if (!USERS.containsKey(id)) { + throw new NotFoundException("user with id: " + id + " not found"); + } + // always throw this exception + throw new ActivationException("user cannot be activate"); + } + + @HystrixCommand( + ignoreExceptions = {BadRequestException.class, NotFoundException.class}, + fallbackMethod = "blockUserFallback") + public void blockUser(String id) throws NotFoundException, OperationException { + validate(id); + if (!USERS.containsKey(id)) { + throw new NotFoundException("user with id: " + id + " not found"); + } + // always throw this exception + throw new OperationException("user cannot be blocked"); } private User fallback(String id) { return failoverService.getDefUser(); } - private void validate(String val) throws NullPointerException, IllegalArgumentException { - if (val == null) { - throw new NullPointerException("parameter cannot be null"); - } else if (val.length() == 0) { - throw new IllegalArgumentException("parameter cannot be empty"); + private void activateFallback(String id) { + failoverService.activate(); + } + + @HystrixCommand(ignoreExceptions = {RuntimeException.class}) + private void blockUserFallback(String id) { + throw new RuntimeOperationException("blockUserFallback has failed"); + } + + private void validate(String val) throws BadRequestException { + if (val == null || val.length() == 0) { + throw new BadRequestException("parameter cannot be null ot empty"); } } } @@ -118,6 +202,40 @@ private class FailoverService { public User getDefUser() { return new User("def", "def"); } + + public void activate() { + } + } + + // exceptions + private static class NotFoundException extends Exception { + private NotFoundException(String message) { + super(message); + } + } + + private static class BadRequestException extends RuntimeException { + private BadRequestException(String message) { + super(message); + } + } + + private static class ActivationException extends Exception { + private ActivationException(String message) { + super(message); + } + } + + private static class OperationException extends Throwable { + private OperationException(String message) { + super(message); + } + } + + private static class RuntimeOperationException extends RuntimeException { + private RuntimeOperationException(String message) { + super(message); + } } } diff --git a/hystrix-contrib/hystrix-javanica/src/test/resources/log4j.properties b/hystrix-contrib/hystrix-javanica/src/test/resources/log4j.properties new file mode 100644 index 000000000..b30279b57 --- /dev/null +++ b/hystrix-contrib/hystrix-javanica/src/test/resources/log4j.properties @@ -0,0 +1,10 @@ +# Define the root logger with appender console +log4j.rootLogger = ERROR, CONSOLE + +# Define the console appender +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.File=${log}/log.out + +# Define the layout for console appender +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.conversionPattern=%m%n \ No newline at end of file