Skip to content

Commit

Permalink
Merge pull request #735 from dmgcodevil/master
Browse files Browse the repository at this point in the history
BatchHystrixCommand has no sence and doesn't collapse requests #68
  • Loading branch information
mattrjacobs committed Apr 6, 2015
2 parents a22afb2 + eb4002e commit 8ac35b9
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 334 deletions.
159 changes: 109 additions & 50 deletions hystrix-contrib/hystrix-javanica/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,76 +380,135 @@ ThreadPoolProperties can be set using @HystrixCommand's 'threadPoolProperties' l

Suppose you have some command which calls should be collapsed in one backend call. For this goal you can use ```@HystrixCollapser``` annotation.

**Asynchronous call**
Example:
```java
@HystrixCommand
@HystrixCollapser
public Future<User> getUserAsync(final String id, final String name) {
return new AsyncResult<User>() {
@Override
public User invoke() {
return new User(id, name + id); // there should be a network call
}
};
@HystrixCollapser(batchMethod = "getUserByIds")
public Future<User> getUserById(String id) {
return null;
}

@HystrixCommand
public List<User> getUserByIds(List<String> ids) {
List<User> users = new ArrayList<User>();
for (String id : ids) {
users.add(new User(id, "name: " + id));
}
return users;
}


Future<User> f1 = userService.getUserById("1");
Future<User> f2 = userService.getUserById("2");
Future<User> f3 = userService.getUserById("3");
Future<User> f4 = userService.getUserById("4");
Future<User> f5 = userService.getUserById("5");
```
A method annotated with ```@HystrixCollapser``` annotation can return any value with compatible type, it does not affect the result of collapser execution, collapser method can even return ```null``` or another stub.
There are several rules applied for methods signatures.

**Synchronous call**
_Note_ : Request collapsing on a single thread makes no sense, because if a single thread is invoking ```execute()``` or ```queue().get()``` synchronously it will block and wait for a response and thus not submit any further requests that will be collapsed inside the window. Doing requests on a separate threads (such as user requests with collapsing scope set to GLOBAL) would make sense:
1. Collapser method must have one argument of any type, desired a wrapper of a primitive type like Integer, Long, String and etc.
2. A batch method must have one argument with type java.util.List parameterized with corresponding type, that's if a type of collapser argument is ```Integer``` then type of batch method argument must be ```List<Integer>```.
3. Return type of batch method must be java.util.List parameterized with corresponding type, that's if a return type of collapser method is ```User``` then a return type of batch command must be ```List<User>```.

```java
@HystrixCommand
@HystrixCollapser(scope = GLOBAL)
public User getUserSync(String id, String name) {
return new User(id, name + id); // there should be a network call
}
```
**Convention for batch method behavior**

To set collapser [properties](https://github.com/Netflix/Hystrix/wiki/Configuration#Collapser) use `@HystrixCollapser#collapserProperties`
The size of response collection must be equal to the size of request collection.

Read more about Hystrix request collapsing [here] (https://github.com/Netflix/Hystrix/wiki/How-it-Works#wiki-RequestCollapsing)
```java
@HystrixCommand
public List<User> getUserByIds(List<String> ids); // batch method

List<String> ids = List("1", "2", "3");
getUserByIds(ids).size() == ids.size();
```
Order of elements in response collection must be same as in request collection.

**Collapser error processing**
```
@HystrixCommand
public List<User> getUserByIds(List<String> ids); // batch method
List<String> ids = List("1", "2", "3");
List<User> users = getUserByIds(ids);
System.out.println(users);
// output
User: id=1
User: id=2
User: id=3
```

All commands are collapsed are instances of ```BatchHystrixCommand```. In BatchHystrixCommand the ```getFallback()``` isn't implemented, even if a collapsed command has fallback ``` @HystrixCommand(fallbackMethod = "fallbackCommand")``` then it never be processed in ```getFallback()```, below this moment is consecrated in detail.
**Why order of elements of request and response collections is important?**

If the processing of a request fails then other requests will not be processed within this collapser and ```setException``` method will be automatically called on requests instances. Generally this is an all or nothing affair. For example, a timeout or rejection of the HystrixCommand would cause failure on all of the batched calls. Thus existence of fallback method does not eliminate the break of all requests and the collapser as a whole. Thus there are two scenarios that you can change using ```@HystrixCollapser#fallbackEnabled``` annotation property.
The reason of this is in reducing logic, basically request elements are mapped one-to-one to response elements. Thus if order of elements of request collection is different then the result of execution can be unpredictable.

When one of the requests has failed:
**Deduplication batch command request parameters**.

**Scenario #1** (```@HystrixCollapser(fallbackEnabled = false)```):
In some cases your batch method can depend on behavior of third-party service or library that skips duplicates in a request. It can be a rest service that expects unique values and ignores duplicates. In this case the size of elements in request collection can be different from size of elements in response collection. It violates one of the behavior principle. To fix it you need manually map request to response, for example:

All requests that were processed successfully will be returned to an user and will be available from Future, other requests including one that failed will not. The fallback of collapsed command will not be executed in a case of any errors.
Short example:
```java
// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<User> users = restClient.getUsersByIds(ids);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
.filter(u -> u.getId().equals(it)).findFirst().get())
.collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;
```

@HystrixCommand(fallbackMethod = "fallback")
@HystrixCollapser(fallbackEnabled = false)
public Future<User> getUserAsync(final String id, final String name) {
// some logic here
};

// .......


Future<User> f1 = userService.getUserAsync("1", "name: ");
Future<User> f2 = userService.getUserAsync("2", "name: ");
Future<User> f3 = userService.getUserAsync("not found", "name"); // not found, exception here
Future<User> f4 = userService.getUserAsync("4", "name: "); // will not be processed
Future<User> f5 = userService.getUserAsync("5", "name: "); // will not be processed
System.out.println(f1.get().getName()); // this line will be executed
System.out.println(f2.get().getName()); // this line will be executed
System.out.println(f3.get().getName()); // this line will not be executed
System.out.println(f4.get().getName()); // this line will not be executed
System.out.println(f5.get().getName()); // this line will not be executed
Same case if you want to remove duplicate elements from request collection before a service call.
Example:
```java
// hava 8
@HystrixCommand
List<User> batchMethod(List<String> ids){
// ids = [1, 2, 2, 3]
List<String> uniqueIds = ids.stream().distinct().collect(Collectors.toList());
// uniqueIds = [1, 2, 3]
List<User> users = restClient.getUsersByIds(uniqueIds);
// users = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
List<User> response = ids.stream().map(it -> users.stream()
.filter(u -> u.getId().equals(it)).findFirst().get())
.collect(Collectors.toList());
// response = [User{id='1', name='user1'}, User{id='2', name='user2'}, User{id='2', name='user2'}, User{id='3', name='user3'}]
return response;
```
To set collapser [properties](https://github.com/Netflix/Hystrix/wiki/Configuration#Collapser) use `@HystrixCollapser#collapserProperties`

**Scenario #2** (```@HystrixCollapser(fallbackEnabled = true)```):
Read more about Hystrix request collapsing [here] (https://github.com/Netflix/Hystrix/wiki/How-it-Works#wiki-RequestCollapsing)

All requests that were processed successfully or failed will be returned to an user and will be available from a Future instance. For failed requests the collapsed command's fallback method will be executed, of course if the name of fallback method was defined in ```@HystrixCommand```. The fallback of collapsed command will be used to process any exceptions during batch processing. If the fallback method hasn't ```@HystrixCommand``` annotation then this method will be processed in the a single thread with the BatchHystrixCommand in method _run()_, otherwise fallback will be process as Hystrix command in separate thread if fallback method was annotated with ```@HystrixCommand```. This is important point that you need to pay attention if you want to use fallback for your collapsed command. Thus fallback method will be processed in ```BatchHystrixCommand#run()``` not in ```BatchHystrixCommand#getFallback()```, but fallback can be process as Hystrix command therefore set the ```fallbackEnabled``` as true can be useful in some situations to avoid crash of whole collapser and continue to process requests, but I recommend to use ```@HystrixCollapser(fallbackEnabled = false)``` and give preference to the first scenario as this is the correct behavior in most cases.
**Collapser error processing**
Bath command can have a fallback method.
Example:

```java
@HystrixCollapser(batchMethod = "getUserByIdsWithFallback")
public Future<User> getUserByIdWithFallback(String id) {
return null;
}

@HystrixCommand(fallbackMethod = "getUserByIdsFallback")
public List<User> getUserByIdsWithFallback(List<String> ids) {
throw new RuntimeException("not found");
}


@HystrixCommand
private List<User> getUserByIdsFallback(List<String> ids) {
List<User> users = new ArrayList<User>();
for (String id : ids) {
users.add(new User(id, "name: " + id));
}
return users;
}
```


#Development Status and Future
Please create an issue if you need a feature or you detected some bugs. Thanks

**Note**: Javaniva 1.4.+ is more stable than 1.3.+ All fixes initially are added in 1.4.+ and after if it's not much efforts then merged to 1.3. **It's recommended to use Javaniva 1.4.+**
**Note**: Javaniva 1.4.+ is updated more frequently than 1.3.+ hence 1.4+ is more stable.

**It's recommended to use Javaniva 1.4.+**
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@
/**
* This annotation is used to collapse some commands into a single backend dependency call.
* This annotation should be used together with {@link HystrixCommand} annotation.
* <p/>
* Example:
* <pre>
* @HystrixCollapser(batchMethod = "getUserByIds"){
* public Future<User> getUserById(String id) {
* return null;
* }
* @HystrixCommand
* public List<User> getUserByIds(List<String> ids) {
* List<User> users = new ArrayList<User>();
* for (String id : ids) {
* users.add(new User(id, "name: " + id));
* }
* return users;
* }
* </pre>
*
* A method annotated with {@link HystrixCollapser} annotation can return any
* value with compatible type, it does not affect the result of collapser execution,
* collapser method can even return {@code null} or another stub.
* Pay attention that if a collapser method returns parametrized Future then generic type must be equal to generic type of List,
* for instance:
* <pre>
* Future<User> - return type of collapser method
* List<User> - return type of batch command method
* </pre>
* <p/>
* Note: batch command method must be annotated with {@link HystrixCommand} annotation.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -41,6 +69,19 @@
*/
String collapserKey() default "";

/**
* Method name of batch command.
* <p/>
* Method must have the following signature:
* <pre>
* java.util.List method(java.util.List)
* </pre>
* NOTE: batch method can have only one argument.
*
* @return method name of batch command
*/
String batchMethod();

/**
* Defines what scope the collapsing should occur within.
* <p/>
Expand All @@ -57,13 +98,4 @@
*/
HystrixProperty[] collapserProperties() default {};

/**
* Used to enabled fallback logic for a batch hystrix command.
* <p/>
* default => the false
*
* @return true if processing of a fallback is allowed
*/
boolean fallbackEnabled() default false;

}
Loading

0 comments on commit 8ac35b9

Please sign in to comment.