Back to Repositories

Testing Hystrix Request Collapsing Implementation in Netflix/Hystrix

This test suite evaluates the Hystrix request collapsing functionality in Netflix’s Hystrix library, focusing on batching multiple concurrent requests into a single execution. It verifies both synchronous and reactive implementations, along with fallback behavior and error handling scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of Hystrix’s request collapsing capabilities.

Key areas tested include:
  • Basic request collapsing with Future return types
  • Reactive request collapsing with Observable return types
  • Fallback behavior with multiple fallback layers
  • Error handling with exception propagation
  • Invalid configuration validation

Implementation Analysis

The testing approach utilizes JUnit to validate Hystrix’s collapser annotations and command execution. Tests verify that multiple individual requests are properly collapsed into batch executions, with assertions checking command execution counts, event types, and result correctness.

Implementation features:
  • @HystrixCollapser annotation usage
  • Batch command execution verification
  • Multiple fallback chain testing
  • Reactive Observable testing

Technical Details

Testing tools and configuration:
  • JUnit test framework
  • Hystrix annotations (@HystrixCommand, @HystrixCollapser)
  • HystrixRequestLog for command execution verification
  • RxJava Observables for reactive tests
  • Custom UserService implementation for test scenarios

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices for distributed systems.

Notable practices include:
  • Thorough validation of success and failure paths
  • Explicit verification of command execution counts
  • Comprehensive error scenario coverage
  • Clear separation of test scenarios
  • Proper cleanup between tests

netflix/hystrix

hystrix-contrib/hystrix-javanica/src/test/java/com/netflix/hystrix/contrib/javanica/test/common/collapser/BasicCollapserTest.java

            
/**
 * Copyright 2016 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.test.common.collapser;

import com.google.common.collect.Sets;
import com.netflix.hystrix.HystrixEventType;
import com.netflix.hystrix.HystrixInvokableInfo;
import com.netflix.hystrix.HystrixRequestLog;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.test.common.BasicHystrixTest;
import com.netflix.hystrix.contrib.javanica.test.common.domain.User;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static com.netflix.hystrix.contrib.javanica.test.common.CommonUtils.getHystrixCommandByKey;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

/**
 * Created by dmgcodevil
 */
public abstract class BasicCollapserTest extends BasicHystrixTest {

    protected abstract UserService createUserService();

    private UserService userService;

    @Before
    public void setUp() throws Exception {
        userService = createUserService();

    }

    @Test
    public void testGetUserById() throws ExecutionException, InterruptedException {

        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");

        assertEquals("name: 1", f1.get().getName());
        assertEquals("name: 2", f2.get().getName());
        assertEquals("name: 3", f3.get().getName());
        assertEquals("name: 4", f4.get().getName());
        assertEquals("name: 5", f5.get().getName());
        // assert that the batch command 'getUserByIds' was in fact
        // executed and that it executed only once
        assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest()
                .getAllExecutedCommands().iterator().next();
        // assert the command is the one we're expecting
        assertEquals("getUserByIds", command.getCommandKey().name());
        // confirm that it was a COLLAPSED command execution
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED));
        // and that it was successful
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS));
    }

    @Test
    public void testReactive() throws Exception {

        final Observable<User> u1 = userService.getUserByIdReactive("1");
        final Observable<User> u2 = userService.getUserByIdReactive("2");
        final Observable<User> u3 = userService.getUserByIdReactive("3");
        final Observable<User> u4 = userService.getUserByIdReactive("4");
        final Observable<User> u5 = userService.getUserByIdReactive("5");

        final Iterable<User> users = Observable.merge(u1, u2, u3, u4, u5).toBlocking().toIterable();

        Set<String> expectedIds = Sets.newHashSet("1", "2", "3", "4", "5");
        for (User cUser : users) {
            assertEquals(expectedIds.remove(cUser.getId()), true);
        }
        assertEquals(expectedIds.isEmpty(), true);
        assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest()
                .getAllExecutedCommands().iterator().next();
        // assert the command is the one we're expecting
        assertEquals("getUserByIds", command.getCommandKey().name());
        // confirm that it was a COLLAPSED command execution
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED));
        // and that it was successful
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS));
    }

    @Test
    public void testGetUserByIdWithFallback() throws ExecutionException, InterruptedException {
        Future<User> f1 = userService.getUserByIdWithFallback("1");
        Future<User> f2 = userService.getUserByIdWithFallback("2");
        Future<User> f3 = userService.getUserByIdWithFallback("3");
        Future<User> f4 = userService.getUserByIdWithFallback("4");
        Future<User> f5 = userService.getUserByIdWithFallback("5");

        assertEquals("name: 1", f1.get().getName());
        assertEquals("name: 2", f2.get().getName());
        assertEquals("name: 3", f3.get().getName());
        assertEquals("name: 4", f4.get().getName());
        assertEquals("name: 5", f5.get().getName());
        // two command should be executed: "getUserByIdWithFallback" and "getUserByIdsWithFallback"
        assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> getUserByIdsWithFallback = getHystrixCommandByKey("getUserByIdsWithFallback");
        com.netflix.hystrix.HystrixInvokableInfo getUserByIdsFallback = getHystrixCommandByKey("getUserByIdsFallback");
        // confirm that command has failed
        assertTrue(getUserByIdsWithFallback.getExecutionEvents().contains(HystrixEventType.FAILURE));
        assertTrue(getUserByIdsWithFallback.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
        // and that fallback was successful
        assertTrue(getUserByIdsFallback.getExecutionEvents().contains(HystrixEventType.SUCCESS));
    }

    @Test
    public void testGetUserByIdWithFallbackWithThrowableParam() throws ExecutionException, InterruptedException {
        Future<User> f1 = userService.getUserByIdWithFallbackWithThrowableParam("1");
        Future<User> f2 = userService.getUserByIdWithFallbackWithThrowableParam("2");
        Future<User> f3 = userService.getUserByIdWithFallbackWithThrowableParam("3");
        Future<User> f4 = userService.getUserByIdWithFallbackWithThrowableParam("4");
        Future<User> f5 = userService.getUserByIdWithFallbackWithThrowableParam("5");

        assertEquals("name: 1", f1.get().getName());
        assertEquals("name: 2", f2.get().getName());
        assertEquals("name: 3", f3.get().getName());
        assertEquals("name: 4", f4.get().getName());
        assertEquals("name: 5", f5.get().getName());
        // 4 commands should be executed
        assertEquals(4, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> batchCommand = getHystrixCommandByKey("getUserByIdsThrowsException");
        com.netflix.hystrix.HystrixInvokableInfo fallback1 = getHystrixCommandByKey("getUserByIdsFallbackWithThrowableParam1");
        com.netflix.hystrix.HystrixInvokableInfo fallback2 = getHystrixCommandByKey("getUserByIdsFallbackWithThrowableParam2");
        com.netflix.hystrix.HystrixInvokableInfo fallback3 = getHystrixCommandByKey("getUserByIdsFallbackWithThrowableParam3");
        // confirm that command has failed
        assertTrue(batchCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));

        assertTrue(fallback1.getExecutionEvents().contains(HystrixEventType.FAILURE));
        assertTrue(fallback2.getExecutionEvents().contains(HystrixEventType.FAILURE));
        assertTrue(fallback2.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));

        // and that last fallback3 was successful
        assertTrue(fallback3.getExecutionEvents().contains(HystrixEventType.SUCCESS));
    }

    @Test(expected = IllegalStateException.class)
    public void testGetUserByIdWrongBatchMethodArgType() {
        userService.getUserByIdWrongBatchMethodArgType("1");
    }

    @Test(expected = IllegalStateException.class)
    public void testGetUserByIdWrongBatchMethodReturnType() {
        userService.getUserByIdWrongBatchMethodArgType("1");
    }

    @Test(expected = IllegalStateException.class)
    public void testGetUserByIdWrongCollapserMethodReturnType() {
        userService.getUserByIdWrongCollapserMethodReturnType("1");
    }

    @Test(expected = IllegalStateException.class)
    public void testGetUserByIdWrongCollapserMultipleArgs() {
        userService.getUserByIdWrongCollapserMultipleArgs("1", "2");
    }

    @Test(expected = IllegalStateException.class)
    public void testGetUserByIdWrongCollapserNoArgs() {
        userService.getUserByIdWrongCollapserNoArgs();
    }

    public static class UserService {

        public static final Logger log = LoggerFactory.getLogger(UserService.class);
        public static final User DEFAULT_USER = new User("def", "def");


        @HystrixCollapser(batchMethod = "getUserByIds",
                collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
        public Future<User> getUserById(String id) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIdsWithFallback",
                collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
        public Future<User> getUserByIdWithFallback(String id) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIds",
                collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
        public Observable<User> getUserByIdReactive(String id) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIdsThrowsException",
                collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
        public Future<User> getUserByIdWithFallbackWithThrowableParam(String id) {
            return null;
        }

        @HystrixCommand(
                fallbackMethod = "getUserByIdsFallbackWithThrowableParam1",
                commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")// for debug
        })
        public List<User> getUserByIdsThrowsException(List<String> ids) {
            throw new RuntimeException("getUserByIdsFails failed");
        }

        @HystrixCommand(fallbackMethod = "getUserByIdsFallbackWithThrowableParam2")
        private List<User> getUserByIdsFallbackWithThrowableParam1(List<String> ids, Throwable e) {
            if (e.getMessage().equals("getUserByIdsFails failed")) {
                throw new RuntimeException("getUserByIdsFallbackWithThrowableParam1 failed");
            }
            List<User> users = new ArrayList<User>();
            for (String id : ids) {
                users.add(new User(id, "name: " + id));
            }
            return users;
        }

        @HystrixCommand(fallbackMethod = "getUserByIdsFallbackWithThrowableParam3")
        private List<User> getUserByIdsFallbackWithThrowableParam2(List<String> ids) {
            throw new RuntimeException("getUserByIdsFallbackWithThrowableParam2 failed");
        }

        @HystrixCommand
        private List<User> getUserByIdsFallbackWithThrowableParam3(List<String> ids, Throwable e) {
            if (!e.getMessage().equals("getUserByIdsFallbackWithThrowableParam2 failed")) {
                throw new RuntimeException("getUserByIdsFallbackWithThrowableParam3 failed");
            }
            List<User> users = new ArrayList<User>();
            for (String id : ids) {
                users.add(new User(id, "name: " + id));
            }
            return users;
        }

        @HystrixCommand(commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")// for debug
        })
        public List<User> getUserByIds(List<String> ids) {
            List<User> users = new ArrayList<User>();
            for (String id : ids) {
                users.add(new User(id, "name: " + id));
            }
            log.debug("executing on thread id: {}", Thread.currentThread().getId());
            return users;
        }

        @HystrixCommand(fallbackMethod = "getUserByIdsFallback",
                commandProperties = {
                        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")// for debug
                })
        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;
        }

        // wrong return type, expected: Future<User> or User, because batch command getUserByIds returns List<User>
        @HystrixCollapser(batchMethod = "getUserByIds")
        public Long getUserByIdWrongCollapserMethodReturnType(String id) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIds")
        public Future<User> getUserByIdWrongCollapserMultipleArgs(String id, String name) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIds")
        public Future<User> getUserByIdWrongCollapserNoArgs() {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIdsWrongBatchMethodArgType")
        public Future<User> getUserByIdWrongBatchMethodArgType(String id) {
            return null;
        }

        // wrong arg type, expected: List<String>
        @HystrixCommand
        public List<User> getUserByIdsWrongBatchMethodArgType(List<Integer> ids) {
            return null;
        }

        @HystrixCollapser(batchMethod = "getUserByIdsWrongBatchMethodReturnType")
        public Future<User> getUserByIdWrongBatchMethodReturnType(String id) {
            return null;
        }

        // wrong return type, expected: List<User>
        @HystrixCommand
        public List<Integer> getUserByIdsWrongBatchMethodReturnType(List<String> ids) {
            return null;
        }

    }
}