Статьи

Hystrix от Netflix: отказоустойчивость в связанном мире

В соединенном мире наши программные решения все чаще состоят из нескольких распределенных компонентов. Сервис-ориентированные архитектуры не являются чем-то новым, и с растущим интересом к микросервисам кажется, что тенденция к распространению в ближайшее время никуда не денется.

При работе с удаленными компонентами необходимо учитывать две (очень) широкие категории проблем:

  1. Во-первых, как мы реагируем на сбои в этих удаленных системах, и 

  2. Во-вторых, как мы управляем нашими вызовами к этим удаленным системам, чтобы поддерживать производительность наших собственных систем и сводить задержки к минимуму

Что такое Hystrix?

Hystrix — это библиотека, изначально разработанная Netflix, которая позволяет вам решать проблемы с задержкой и отказоустойчивостью в сложных распределенных системах. Предполагается, что в нем будут происходить всевозможные сбои, и он предоставляет ряд инструментов и решений для работы с различными распространенными сценариями.

Hystrix имеет открытый исходный код и доступен на Github. По их собственным словам — «Hystrix — это библиотека задержек и отказоустойчивости, предназначенная для изоляции точек доступа к удаленным системам, службам и сторонним библиотекам, предотвращения каскадного сбоя и обеспечения устойчивости в сложных распределенных системах, где сбой неизбежен».

Демонстрационное приложение — сервис ставок

Я создал простое демонстрационное приложение, которое доступно на Github. Нажмите на ссылку ниже:

Пример приложения использует простой BettingService, чтобы проиллюстрировать некоторые инструменты, доступные при использовании Hystrix для доступа к удаленному сервису.

BettingService.java

package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**
 * Simulates the interface for a remote betting service. 
 *
*/

public interface BettingService {

/**
 * Get a list the names of all Race courses with races on today.
 * @return List of race course names
*/

List<Racecourse> getTodaysRaces();

/**
 * Get a list of all Horses running in a particular race.
 * @param race Name of race course
 * @return List of the names of all horses running in the specified race
*/

List<Horse> getHorsesInRace(String raceCourseId);

/**
 * Get current odds for a particular horse in a specific race today.
 * @param race Name of race course
 * @param horse Name of horse
 * @return Current odds as a string (e.g. "10/1")
*/

String getOddsForHorse(String raceCourseId, String horseId);
}

Сервис выставляет 2 доменных объекта — Racecourse и Horse:

Racecourse.java

package com.cor.hysterix.domain;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
public class Racecourse {
private String id;
private String name;

public Racecourse(String id, String name) {
super();
this.id = id;
this.name = name;
}

  public String getId() {
return id;
}

  public void setId(String id) {
this.id = id;
}

  public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override

  public String toString() {
return "Racecourse [id=" + id + ", name=" + name + "]";
}

@Override

  public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

  @Override

  public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}

}

Horse.java

package com.cor.hysterix.domain;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
public class Horse {
private String id;
private String name;
public Horse(String id, String name) {
super();
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override

public String toString() {
return "Horse [id=" + id + ", name=" + name + "]";
}

@Override

public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

@Override

public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}

}

Подход Хистерикса

Обработка ошибок

Когда приложение сильно зависит от других сервисов, существует опасность того, что любой сбой в этих системах может отрицательно повлиять на наше вызывающее приложение. Как разработчики вызывающего приложения, коренные причины этих проблем в значительной степени находятся вне нашего контроля, но то, как мы их решаем, имеет решающее значение для эффективной работы наших собственных систем.

Hysterix в основном построен на основе шаблона проектирования Command. Каждый вызов удаленной службы заключен в HysterixCommand (для синхронных вызовов) или HysterixObserveableCommand (для асинхронных вызовов)

Простые неудачи

В самом простом из сценариев — в случае, когда удаленная служба недоступна или выдает исключение, HysterixCommand позволяет вам определить, какое действие вы хотите предпринять в методе getFallback () (это может быть для распространения исключения, возврата значения по умолчанию). или специальное значение ошибки).

Автоматический выключатель — быстрый отказ

Вызовы в удаленную систему могут быть дорогостоящими — даже если система возвращает ошибку, может существовать период ожидания в ожидании возврата ошибки. Это может привести к задержке в вызывающей системе, а также к ресурсам борова — возможно, даже израсходовать все потоки в пуле потоков (подробнее об этом ниже).

Hystrix вводит концепцию автоматического выключателя, чтобы облегчить быстрый отказ. Если он обнаруживает ряд подобных сбоев в быстрой последовательности (некоторая комбинация громкости и ошибки), он может отключить автоматический выключатель, заставив все последующие вызовы быстро завершиться с ошибкой без выполнения удаленных вызовов на удаленный сервер (вызов через функцию HystricCommands getFallback ( ) метод, описанный выше). По истечении некоторого настраиваемого периода он снова замыкает цепь и начинает обрабатывать удаленные вызовы (опять же, если он все еще обнаруживает состояние ошибки, он снова открывает автоматический выключатель и т. Д.). Это позволяет избежать слепого вызова удаленной системы, когда мы знаем, что с ней, очевидно, есть проблема — устранение задержки и разрешение нам справиться с ней заранее определенным методом — который может включать в себя возврат кэшированных данных или переключение на резервную систему.

Латентность и потоки потоков

Как уже упоминалось выше, гораздо более коварная сторона сбоев (или даже успешных, но медленных вызовов) — это возможность использовать все потоки в пуле вызовов. Поскольку каждый вызов блокирует поток во время ожидания ответа от удаленной системы, новые вызовы используют больше потоков из пула. Если этот пул не изолирован от основного пула серверов приложений — он может привести к остановке всего вызывающего приложения.

Hystrix позволяет использовать свой собственный пул (ы) потоков — поэтому потоки вызывающих приложений никогда не рискуют быть использованными, что позволяет ему завершать вызовы, которые занимают слишком много времени. Различные команды или группы команд могут быть сконфигурированы со своими собственными пулами потоков, поэтому можно изолировать различные наборы вызовов службы (например, если у удаленной службы A возникают проблемы с задержкой, утечки влияния на вызовы удаленной службы B отсутствуют). ). Это помогает изолировать и управлять вызовами в разных клиентских библиотеках и сетях.

Однако использование пулов потоков накладных расходов. Netflix разработал свои собственные метрики (более 10 миллиардов выполнений команд Hystrix в день с использованием изоляции потоков) и пришел к выводу, что издержки в их системах были настолько малы, что преимущества изоляции перевесили их.

Архитектура, используемая Hystrix, показана на диаграмме ниже. Каждая зависимость изолирована друг от друга, ограничена в ресурсах, которые она может насытить, когда возникает задержка, и покрыта резервной логикой, которая решает, какой ответ следует предпринять, когда в зависимости возникает любой тип ошибки:

Эффективное кодирование вызовов клиентов

Обратной стороной реагирования на сбои является максимально эффективное кодирование удаленных вызовов, чтобы попытаться свести к минимуму проблемы с задержкой / нагрузкой.

Опять же, Hystrix предоставляет некоторые инструменты в библиотеке, чтобы помочь справиться с этим.

Запрос рушится

Если вы представляете многократное использование нашего клиентского приложения, использующего систему одновременно, вполне вероятно, что действия некоторых пользователей приведут к тому, что аналогичные запросы будут отправлены удаленной службе почти одновременно (например, получить цену на акции Apple Inc NASDAQ: AAPL). Без какого-либо вмешательства — это приведет к тому, что практически повторяющиеся запросы будут отправляться по сети практически в реальном времени.

Свертывание запросов вводит небольшой период ожидания между тем, когда запрашивается удаленный вызов службы, и когда он выполняется — чтобы увидеть, выполняются ли какие-либо дублирующие запросы в окне (это окно обычно очень мало — например, 10 мс). Существует возможность использовать «глобальный» контекст (т.е. свернуть все запросы для любого пользователя в любом потоке Tomcat) или контекст «Пользовательский запрос» (т.е. свернуть все запросы для одного потока Tomcat).

Хотя это полезный инструмент — свертывание запросов действительно полезно только в сценариях, когда определенная команда вызывается с большой частотой, что позволяет ей объединять десятки или даже сотни вызовов вместе, уменьшая потоки и нагрузку на сеть. Это особенно верно для вызовов с высокой задержкой, когда накладные расходы, связанные с ожиданием нескольких миллисекунд для свертывающегося окна, не оказывают реального влияния на общее время ответа на вызов.

Для нечастых команд или команд с низкой задержкой стоимость свертываний перевесит ее преимущества.

Запрос кеширования

Кэширование запросов помогает гарантировать, что разные потоки не будут выполнять дублирующие / избыточные вызовы внешней службы. Поскольку кэш находится перед методами построения / выполнения, это означает, что Hystrix может возвращать кэшированные результаты перед созданием и выполнением нового потока для каждого дублированного запроса. Кэширование можно легко добавить в HystrixCommand или HystrixObserveableCommand, просто переопределив метод getCacheKey () (это предоставит ключ, который Hystrix использует для определения, является ли последующий запрос к команде дубликатом предыдущего).

Примеры от службы ставок

Вызов getTodaysRaces

Это звонок, который клиент может сделать в Службе ставок, чтобы получить список всех рас, доступных в этот день. Просто простейший пример расширения HystrixCommand для доступа к удаленной службе (примеры того, как ее вызвать, см. В разделе «Модульные тесты» ниже).

CommandGetTodaysRaces.java

package com.cor.hysterix.command;
import java.util.ArrayList;
import java.util.List;
import com.cor.hysterix.domain.Racecourse;
import com.cor.hysterix.exception.RemoteServiceException;
import com.cor.hysterix.service.BettingService;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixThreadPoolKey;

/**
* Get a list of all Race courses with races on today.
*
*/

public class CommandGetTodaysRaces extends HystrixCommand<List<Racecourse>> {
private final BettingService service;
private final boolean failSilently;

/**
* CommandGetTodaysRaces
*
* @param service
* Remote Broker Service
* @param failSilently
* If <code>true</code> will return an empty list if a remote service exception is thrown, if
* <code>false</code> will throw a BettingServiceException.
*/

public CommandGetTodaysRaces(BettingService service, boolean failSilently) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup")).andThreadPoolKey(
HystrixThreadPoolKey.Factory.asKey("BettingServicePool")));
this.service = service;
this.failSilently = failSilently;
}

public CommandGetTodaysRaces(BettingService service) {
this(service, true);
}

@Override

protected List<Racecourse> run() {
return service.getTodaysRaces();
}

@Override

protected List<Racecourse> getFallback() {

// can log here, throw exception or return default
if (failSilently) {
return new ArrayList<Racecourse>();
} else {
throw new RemoteServiceException("Unexpected error retrieving todays races");
}

}
}

Вызов GetHorsesInRaceWithCaching

Это вызов, который клиент может сделать в Службе ставок, чтобы получить список всех лошадей, участвующих в конкретной гонке. Я использовал это как пример того, как реализовать кэширование путем переопределения метода getCacheKey () в HystrixCommand. В этом примере мы используем raceId в качестве ключа кеша. Подробности о кеше, истечении срока действия и т. Д. Можно настроить подробно (но это выходит за рамки данной статьи).

CommandGetHorsesInRaceWithCaching.java

package com.cor.hysterix.command.caching;
import java.util.ArrayList;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.service.BettingService;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixThreadPoolKey;

/**
* Get List of Horses in a specific race. Note, calls via this command are cached.
*
*/

public class CommandGetHorsesInRaceWithCaching extends HystrixCommand<List<Horse>> {

private final BettingService service;
private final String raceCourseId;

/**
* CommandGetHorsesInRaceWithCaching.
* @param service
* Remote Broker Service
* @param raceCourseId
* Id of race course
*/

public CommandGetHorsesInRaceWithCaching(BettingService service, String raceCourseId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup")).andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("BettingServicePool")));
this.service = service;
this.raceCourseId = raceCourseId;
}

@Override

protected List<Horse> run() {
return service.getHorsesInRace(raceCourseId);
}

@Override

protected List<Horse> getFallback() {

    // can log here, throw exception or return default

return new ArrayList<Horse>();
}

@Override

protected String getCacheKey() {
return raceCourseId;
}
}

Вот некоторые модульные тесты, иллюстрирующие использование этой команды различными способами:

  • testSynchronous () — показывает основной синхронный вызов для использования команды

  • testSynchronousFailSilently() — shows a basic synchronous call which captures an Exception from the remote service, swallows it and returns an empty list

  • testSynchronousFailFast() — shows a basic synchronous call which captures an Exception from the remote service, and throws a new RemoteServiceException

  • testAsynchronous() — shows a basic asynchronous call using Futures to use the same command

  • testObservable() — shows a basic asynchronous call using Observables to use the same command

  • testWithCacheHits() — illustrates how the Command caches the response from the server, thus reducing unnecessary remote calls

package com.cor.hysterix;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;

import org.junit.Before;
import org.junit.Test;

import rx.Observable;

import com.cor.hysterix.command.CommandGetTodaysRaces;
import com.cor.hysterix.command.batching.CommandCollapserGetOddsForHorse;
import com.cor.hysterix.command.batching.GetOddsForHorseRequest;
import com.cor.hysterix.command.caching.CommandGetHorsesInRaceWithCaching;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;
import com.cor.hysterix.exception.RemoteServiceException;
import com.cor.hysterix.service.BettingService;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixEventType;
import com.netflix.hystrix.HystrixRequestCache;
import com.netflix.hystrix.HystrixRequestLog;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;

/**
* Unit Test Suite that illustrates various approaches in using Hystrix Commands to access remote services.
*
* Uses Mockito to wire in a mock remote betting service.
*
*/

public class BettingServiceTest {
private static final String RACE_1 = "course_aintree";
private static final String HORSE_1 = "horse_redrum";
private static final String HORSE_2 = "horse_shergar";
private static final String ODDS_RACE_1_HORSE_1 = "10/1";
private static final String ODDS_RACE_1_HORSE_2 = "100/1";
private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("GetterCommand");
private BettingService mockService;

/**
* Set up the shared Unit Test environment
*/

@Before

  public void setUp() {
mockService = mock(BettingService.class);
when(mockService.getTodaysRaces()).thenReturn(getRaceCourses());
when(mockService.getHorsesInRace(RACE_1)).thenReturn(getHorsesAtAintree());
when(mockService.getOddsForHorse(RACE_1, HORSE_1)).thenReturn(ODDS_RACE_1_HORSE_1);
when(mockService.getOddsForHorse(RACE_1, HORSE_2)).thenReturn(ODDS_RACE_1_HORSE_2);
}

/**
* Command GetRaces - Execute (synchronous call).
*/

@Test

public void testSynchronous() {
CommandGetTodaysRaces commandGetRaces = new CommandGetTodaysRaces(mockService);
assertEquals(getRaceCourses(), commandGetRaces.execute());
verify(mockService).getTodaysRaces();
verifyNoMoreInteractions(mockService);
}

/**
* Command GetRaces - Execute and Fail Silently.
* Swallows remote server error and returns an empty list.
*/

@Test

public void testSynchronousFailSilently() {
CommandGetTodaysRaces commandGetRacesFailure = new CommandGetTodaysRaces(mockService);

    // override mock to mimic an error being thrown for this test
when(mockService.getTodaysRaces()).thenThrow(new RuntimeException("Error!!"));
assertEquals(new ArrayList<Racecourse>(), commandGetRacesFailure.execute());

// Verify

verify(mockService).getTodaysRaces();
verifyNoMoreInteractions(mockService);
}

/**
* Command GetRaces - Execute and Fail Fast.
* Catches remote server error and throws a new Exception.
*/

@Test

public void testSynchronousFailFast() {
CommandGetTodaysRaces commandGetRacesFailure = new CommandGetTodaysRaces(mockService, false);
// override mock to mimic an error being thrown for this test
when(mockService.getTodaysRaces()).thenThrow(new RuntimeException("Error!!"));
try{
commandGetRacesFailure.execute();
}
        catch(HystrixRuntimeException hre){
assertEquals(RemoteServiceException.class, hre.getFallbackException().getClass());
}
verify(mockService).getTodaysRaces();
verifyNoMoreInteractions(mockService);
}

/**
* Command GetRaces - Queue (Asynchronous)
*/

@Test

  public void testAsynchronous() throws Exception {
CommandGetTodaysRaces commandGetRaces = new CommandGetTodaysRaces(mockService);
Future<List<Racecourse>> future = commandGetRaces.queue();
assertEquals(getRaceCourses(), future.get());
verify(mockService).getTodaysRaces();
verifyNoMoreInteractions(mockService);
}

/**
* Command - Observe (Hot Observable)
*/

@Test

  public void testObservable() throws Exception {
CommandGetTodaysRaces commandGetRaces = new CommandGetTodaysRaces(mockService);
Observable<List<Racecourse>> observable = commandGetRaces.observe();
    // blocking observable
assertEquals(getRaceCourses(), observable.toBlocking().single());
verify(mockService).getTodaysRaces();
verifyNoMoreInteractions(mockService);
}

/**
* Test - GetHorsesInRace - Uses Caching
*/

@Test

  public void testWithCacheHits() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
CommandGetHorsesInRaceWithCaching commandFirst = new CommandGetHorsesInRaceWithCaching(mockService, RACE_1);
CommandGetHorsesInRaceWithCaching commandSecond = new CommandGetHorsesInRaceWithCaching(mockService, RACE_1);
commandFirst.execute();
// this is the first time we've executed this command with
// the value of "2" so it should not be from cache
assertFalse(commandFirst.isResponseFromCache());
verify(mockService).getHorsesInRace(RACE_1);
verifyNoMoreInteractions(mockService);
commandSecond.execute();
// this is the second time we've executed this command with
// the same value so it should return from cache
assertTrue(commandSecond.isResponseFromCache());
} 
      finally {
context.shutdown();
        }
// start a new request context
context = HystrixRequestContext.initializeContext();
try {
CommandGetHorsesInRaceWithCaching commandThree = new CommandGetHorsesInRaceWithCaching(mockService, RACE_1);
commandThree.execute();
// this is a new request context so this
// should not come from cache
assertFalse(commandThree.isResponseFromCache());
// Flush the cache
HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(RACE_1);
} 
      finally {
context.shutdown();
}
}

  private List<Racecourse> getRaceCourses(){
Racecourse course1 = new Racecourse(RACE_1, "Aintree");
return Arrays.asList(course1);
}

private List<Horse> getHorsesAtAintree(){
Horse horse1 = new Horse(HORSE_1, "Red Rum");
Horse horse2 = new Horse(HORSE_2, "Shergar");
return Arrays.asList(horse1, horse2);
}

// more tests here - see below
}

Calling ‘GetOddsForHorse’

Now this last command is a bit more advanced — utilising batching and request collapsing. As this may be called repeatedly in rapid succession — and we aren’t too concerned if their is some very minor latency in the response — we don’t want to flood the service with similar requests. Using collapsing will introduce a very slight (millseconds) delay after a command is executed so it can wait to see if the same request comes through again. If it does it can make a single call to the service and return the same result to all requesters. Again, the details of this are configurable.

The classes below show how this is done for our particular example, and their is also a Unit Test which exercises this and confirms it is collapsing requests.

GetOddsForHorseRequest.java

package com.cor.hysterix.command.batching;

/**
* Object to wrap the parameters required by the method call {@link BettingService#getOddsForHorse}.
*/

public class GetOddsForHorseRequest {
private final String raceCourseId;
private final String horseId;
public GetOddsForHorseRequest(String raceCourseId, String horseId){
this.raceCourseId = raceCourseId;
this.horseId = horseId;
}

public String getRaceCourseId() {
return raceCourseId;
}

public String getHorseId() {
return horseId;
}
}

BatchCommandGetOddsForHorse.java

package com.cor.hysterix.command.batching;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.cor.hysterix.service.BettingService;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixCollapser.CollapsedRequest;

public class BatchCommandGetOddsForHorse extends HystrixCommand<List<String>> {
private final Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests;
private BettingService service;
public BatchCommandGetOddsForHorse(Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup"))
.andThreadPoolKey(
HystrixThreadPoolKey.Factory.asKey("GetOddsPool")));
this.requests = requests;
}

@Override

  protected List<String> run() {
ArrayList<String> response = new ArrayList<String>();
for (CollapsedRequest<String, GetOddsForHorseRequest> request : requests) {
GetOddsForHorseRequest callRequest = request.getArgument();
response.add(service.getOddsForHorse(callRequest.getRaceCourseId(), callRequest.getHorseId()));
}
return response;
}

public void setService(BettingService service) {
this.service = service;
}
}

CommandCollapserGetOddsForHorse.java

package com.cor.hysterix.command.batching;
import java.util.Collection;
import java.util.List;
import com.cor.hysterix.service.BettingService;
import com.netflix.hystrix.HystrixCollapser;
import com.netflix.hystrix.HystrixCommand;
public class CommandCollapserGetOddsForHorse extends HystrixCollapser<List<String>, String, GetOddsForHorseRequest> {

private final GetOddsForHorseRequest key;
private BettingService service;
public CommandCollapserGetOddsForHorse(GetOddsForHorseRequest key) {
this.key = key;
}

@Override

public GetOddsForHorseRequest getRequestArgument() {
return key;
}

@Override

protected HystrixCommand<List<String>> createCommand(final Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
BatchCommandGetOddsForHorse command = new BatchCommandGetOddsForHorse(requests);
command.setService(service);
return command;
}

@Override

protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
int count = 0;
for (CollapsedRequest<String, GetOddsForHorseRequest> request : requests) {
request.setResponse(batchResponse.get(count++));
}
}

public void setService(BettingService service) {
this.service = service;
}
}

Unit Test ‘CommandCollapserGetOddsForHorse’

The code below shows the Unit Test to check that requests are being collapsed when executing this particular command.

/**
* Request Collapsing
*/

@SuppressWarnings("deprecation")
@Test

public void testCollapser() throws Exception {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
CommandCollapserGetOddsForHorse c1 = new CommandCollapserGetOddsForHorse(new GetOddsForHorseRequest(RACE_1,HORSE_1));
CommandCollapserGetOddsForHorse c2 = new CommandCollapserGetOddsForHorse(new GetOddsForHorseRequest(RACE_1,HORSE_1));
CommandCollapserGetOddsForHorse c3 = new CommandCollapserGetOddsForHorse(new GetOddsForHorseRequest(RACE_1,HORSE_1));
CommandCollapserGetOddsForHorse c4 = new CommandCollapserGetOddsForHorse(new GetOddsForHorseRequest(RACE_1,HORSE_1));
c1.setService(mockService);
c2.setService(mockService);
c3.setService(mockService);
c4.setService(mockService);
try {
Future<String> f1 = c1.queue();
Future<String> f2 = c2.queue();
Future<String> f3 = c3.queue();
Future<String> f4 = c4.queue();
assertEquals(ODDS_RACE_1_HORSE_1, f1.get());
assertEquals(ODDS_RACE_1_HORSE_1, f2.get());
assertEquals(ODDS_RACE_1_HORSE_1, f3.get());
assertEquals(ODDS_RACE_1_HORSE_1, f4.get());
// assert that the batch command 'BatchCommandGetOddsForHorse' was in fact
// executed and that it executed only once
assertEquals(1, HystrixRequestLog.getCurrentRequest().getExecutedCommands().size());
HystrixCommand<?> command = HystrixRequestLog.getCurrentRequest().getExecutedCommands()
.toArray(new HystrixCommand<?>[1])[0];
// assert the command is the one we're expecting
assertEquals("BatchCommandGetOddsForHorse", 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));
} 
  finally {
context.shutdown();
}
}

Conclusion

If you are doing any kind of work where you are accessing remote services — I’d definitely recommend checking Hystrix out. At the most basic level — it forces you to think about all the problems that can occur when accessing services early in the development phase — when solutions are most easy to implement. All too often these calls are developed in an ‘ideal world’ environment, with problems only becoming apparent when they move into user acceptance testing, or, worse still, production.

This article is just an attempt to try out a few of the approaches used by Hystrix — and give a working project to help get started with playing around with it. Check it out on Github — run the Unit Test to exercise the mocked service calls.

Hystrix itself is open source — and can be found on Github — with a wiki and more detailed explanations, here.

So just…