├── .gitignore ├── src ├── main │ ├── docs │ │ ├── geecon.odp │ │ └── dashboard.jpg │ └── resources │ │ └── logback.xml └── test │ ├── groovy │ └── com │ │ └── nurkiewicz │ │ └── hystrix │ │ ├── H52_Turbine.groovy │ │ ├── H50_Statistics_stream.groovy │ │ ├── H51_Dashboard.groovy │ │ ├── H70_Graphite.groovy │ │ ├── H40_Throttling.groovy │ │ ├── H00_About.groovy │ │ ├── H10_Introduction.groovy │ │ ├── H41_Semaphores.groovy │ │ ├── H21_Timeouts.groovy │ │ ├── H11_Timeouts.groovy │ │ ├── H20_Basic_Hystrix.groovy │ │ ├── H81_ObservableCommand.groovy │ │ ├── H23_HystrixGuard.groovy │ │ ├── H22_Fallback.groovy │ │ ├── H80_Observables.groovy │ │ ├── H12_Nines.groovy │ │ ├── H60_Request_caching.groovy │ │ ├── H30_Circuit_breaker.groovy │ │ ├── MainApplication.groovy │ │ └── H61_Request_batching.groovy │ └── java │ └── com │ └── nurkiewicz │ └── hystrix │ ├── stock │ ├── StockPriceGateway.java │ ├── Ticker.java │ ├── StockPriceCommand.java │ ├── StockPricesBatchCommand.java │ ├── StockController.java │ ├── StockPrice.java │ ├── StockTickerPriceCollapsedCommand.java │ └── SimulatedStockPriceGateway.java │ ├── CustomDownloadCommand.java │ ├── PooledCommand.java │ ├── SemaphoreCommand.java │ ├── Dashboard.java │ ├── QueryParams.java │ ├── ExternalService.java │ ├── ThrottlingController.java │ └── CircuitController.java ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | -------------------------------------------------------------------------------- /src/main/docs/geecon.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nurkiewicz/hystrix-demo/HEAD/src/main/docs/geecon.odp -------------------------------------------------------------------------------- /src/main/docs/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nurkiewicz/hystrix-demo/HEAD/src/main/docs/dashboard.jpg -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H52_Turbine.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * - Aggregating multiple streams 7 | * - Plugin-based discovery 8 | */ 9 | class H52_Turbine extends Specification { 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H50_Statistics_stream.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * $ curl localhost:8080/hystrix.stream 7 | * @see com.nurkiewicz.hystrix.Dashboard 8 | */ 9 | class H50_Statistics_stream extends Specification { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H51_Dashboard.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * $ git clone git@github.com:Netflix/Hystrix.git 7 | * $ cd hystrix-dashboard 8 | * $ ../gradlew jettyRun 9 | */ 10 | class H51_Dashboard extends Specification { 11 | 12 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockPriceGateway.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | 7 | public interface StockPriceGateway { 8 | 9 | StockPrice load(Ticker ticker); 10 | 11 | Map loadMany(Collection tickers); 12 | 13 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H70_Graphite.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import spock.lang.Specification 4 | 5 | 6 | /** 7 | * Long-term history of Hystrix statistics 8 | * 9 | * $ docker run -p 8081:80 -p 2003:2003 --name grafana kamon/grafana_graphite 10 | * $ docker start -a grafana 11 | */ 12 | class H70_Graphite extends Specification { 13 | 14 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H40_Throttling.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * Thread pool and queue size 7 | * @see com.nurkiewicz.hystrix.ThrottlingController 8 | */ 9 | class H40_Throttling extends Specification { 10 | 11 | def 'customized pool and queue size'() { 12 | given: 13 | PooledCommand command = new PooledCommand() 14 | expect: 15 | command.execute() == null 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %d{HH:mm:ss.SSS} | %thread | %-5level | %logger{1} | %msg%n 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __ __ __ _ 2 | / // /_ _____ / /_____(_)_ __ 3 | / _ / // (_- { 12 | 13 | private final URL url; 14 | 15 | protected CustomDownloadCommand(URL url) { 16 | super(HystrixCommandGroupKey.Factory.asKey("Download"), 10_000000); 17 | this.url = url; 18 | } 19 | 20 | @Override 21 | protected String run() throws Exception { 22 | try (InputStream input = url.openStream()) { 23 | final String html = IOUtils.toString(input, StandardCharsets.UTF_8); 24 | return html; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockPriceCommand.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixCommandKey; 6 | 7 | public class StockPriceCommand extends HystrixCommand { 8 | 9 | private final StockPriceGateway gateway; 10 | private final Ticker stock; 11 | 12 | public StockPriceCommand(StockPriceGateway gateway, Ticker stock) { 13 | super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("Stock")) 14 | .andCommandKey(HystrixCommandKey.Factory.asKey("load"))); 15 | this.gateway = gateway; 16 | this.stock = stock; 17 | } 18 | 19 | @Override 20 | protected StockPrice run() throws Exception { 21 | return gateway.load(stock); 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/PooledCommand.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixThreadPoolProperties; 6 | 7 | class PooledCommand extends HystrixCommand { 8 | 9 | protected PooledCommand() { 10 | super(Setter 11 | .withGroupKey(HystrixCommandGroupKey.Factory.asKey("Download")) 12 | .andThreadPoolPropertiesDefaults( 13 | HystrixThreadPoolProperties.Setter() 14 | .withCoreSize(4) 15 | .withMaxQueueSize(20) 16 | .withQueueSizeRejectionThreshold(15) 17 | )); 18 | } 19 | 20 | @Override 21 | protected String run() throws Exception { 22 | return null; 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockPricesBatchCommand.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixCommandKey; 6 | 7 | import java.util.Collection; 8 | import java.util.Map; 9 | 10 | class StockPricesBatchCommand extends HystrixCommand> { 11 | 12 | private final StockPriceGateway gateway; 13 | private final Collection stocks; 14 | 15 | StockPricesBatchCommand(StockPriceGateway gateway, Collection stocks) { 16 | super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("Stock")) 17 | .andCommandKey(HystrixCommandKey.Factory.asKey("loadAll"))); 18 | this.gateway = gateway; 19 | this.stocks = stocks; 20 | } 21 | 22 | @Override 23 | protected Map run() throws Exception { 24 | return gateway.loadMany(stocks); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H41_Semaphores.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import com.netflix.hystrix.HystrixCommandKey 6 | import com.netflix.hystrix.HystrixCommandProperties 7 | import com.netflix.hystrix.HystrixThreadPoolProperties 8 | import groovy.util.logging.Slf4j 9 | import spock.lang.Specification 10 | import com.netflix.hystrix.HystrixCommand.Setter 11 | 12 | 13 | /** 14 | * Used when request volume is really big 15 | * Thread pool overhead is intolerable 16 | * Use only when underlying action is known to be fast 17 | * ...or encapsulate other Hystrix command 18 | */ 19 | class H41_Semaphores extends Specification { 20 | 21 | def 'semaphores provide smaller overhead'() { 22 | given: 23 | SemaphoreCommand command = new SemaphoreCommand() 24 | when: 25 | String result = command.execute() 26 | then: 27 | Thread.currentThread().name == result 28 | } 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H21_Timeouts.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import org.apache.commons.io.IOUtils 6 | import spock.lang.Specification 7 | 8 | import java.nio.charset.StandardCharsets 9 | 10 | class H21_Timeouts extends Specification { 11 | 12 | def 'Minimal Hystrix API'() { 13 | given: 14 | HystrixCommand command = new TimeoutDownloadCommand() 15 | 16 | expect: 17 | String result = command.execute() 18 | 19 | } 20 | 21 | static class TimeoutDownloadCommand extends HystrixCommand { 22 | 23 | protected TimeoutDownloadCommand() { 24 | super(HystrixCommandGroupKey.Factory.asKey("Download"), 5_000) 25 | } 26 | 27 | @Override 28 | protected String run() throws Exception { 29 | URL url = "http://www.example.com".toURL() 30 | InputStream input = url.openStream() 31 | IOUtils.toString(input, StandardCharsets.UTF_8) 32 | } 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H11_Timeouts.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import org.apache.commons.io.IOUtils 4 | import spock.lang.Specification 5 | 6 | import java.nio.charset.StandardCharsets 7 | import java.util.concurrent.* 8 | 9 | /** 10 | * Timeouts for unsafe code 11 | * - Limit max duration 12 | * - You always pay the price of slow query / fail fast 13 | * - Circuit breaking 14 | * - Missing metrics: avg time, queue length 15 | */ 16 | class H11_Timeouts extends Specification { 17 | 18 | def 'manual timeouts'() { 19 | given: 20 | ExecutorService pool = Executors.newFixedThreadPool(10) 21 | when: 22 | Future future = pool.submit(new DownloadTask()) 23 | then: 24 | future.get(1, TimeUnit.SECONDS) 25 | cleanup: 26 | pool.shutdownNow() 27 | } 28 | 29 | static class DownloadTask implements Callable { 30 | 31 | @Override 32 | String call() throws Exception { 33 | URL url = "http://www.example.com".toURL() 34 | InputStream input = url.openStream() 35 | IOUtils.toString(input, StandardCharsets.UTF_8) 36 | } 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H20_Basic_Hystrix.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import org.apache.commons.io.IOUtils 6 | import spock.lang.Specification 7 | 8 | import java.nio.charset.StandardCharsets 9 | 10 | /** 11 | * Basic API example 12 | * - running in a separate thread pool 13 | * - default timeout of one second 14 | */ 15 | class H20_Basic_Hystrix extends Specification { 16 | 17 | def 'Minimal Hystrix API'() { 18 | given: 19 | HystrixCommand command = new BasicDownloadCommand() 20 | 21 | expect: 22 | String result = command.execute() 23 | 24 | } 25 | 26 | static class BasicDownloadCommand extends HystrixCommand { 27 | 28 | protected BasicDownloadCommand() { 29 | super(HystrixCommandGroupKey.Factory.asKey("Download")) 30 | } 31 | 32 | @Override 33 | protected String run() throws Exception { 34 | URL url = "http://www.example.com".toURL() 35 | InputStream input = url.openStream() 36 | return IOUtils.toString(input, StandardCharsets.UTF_8) 37 | } 38 | } 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H81_ObservableCommand.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommandGroupKey 4 | import com.netflix.hystrix.HystrixObservableCommand 5 | import rx.Observable 6 | import spock.lang.Specification 7 | 8 | import java.util.concurrent.TimeUnit 9 | 10 | /** 11 | * Used for non-blocking, reactive commands 12 | */ 13 | class H81_ObservableCommand extends Specification { 14 | 15 | def 'observable command does not occupy threads'() { 16 | given: 17 | HystrixObservableCommand command = new ObservableDownloadCommand() 18 | when: 19 | Observable observe = command.observe() 20 | then: 21 | observe 22 | .toList() 23 | .toBlocking() 24 | .first() == ['Result text'] 25 | } 26 | 27 | static class ObservableDownloadCommand extends HystrixObservableCommand { 28 | 29 | protected ObservableDownloadCommand() { 30 | super(HystrixCommandGroupKey.Factory.asKey("Download")) 31 | } 32 | 33 | @Override 34 | protected Observable construct() { 35 | return Observable 36 | .timer(100, TimeUnit.MILLISECONDS) 37 | .map{x -> 'Result text'} 38 | } 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/SemaphoreCommand.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixCommandKey; 6 | import com.netflix.hystrix.HystrixCommandProperties; 7 | 8 | import static com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE; 9 | 10 | class SemaphoreCommand extends HystrixCommand { 11 | 12 | protected SemaphoreCommand() { 13 | super(Setter 14 | .withGroupKey(HystrixCommandGroupKey.Factory.asKey("Download")) 15 | .andCommandKey(HystrixCommandKey.Factory.asKey("SemCommand")) 16 | .andCommandPropertiesDefaults( 17 | HystrixCommandProperties.Setter() 18 | .withExecutionIsolationStrategy(SEMAPHORE) 19 | .withExecutionIsolationSemaphoreMaxConcurrentRequests(10) 20 | ) 21 | ); 22 | } 23 | 24 | @Override 25 | protected String run() throws Exception { 26 | return Thread.currentThread().getName(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/Dashboard.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.codahale.metrics.MetricRegistry; 4 | import com.codahale.metrics.graphite.Graphite; 5 | import com.codahale.metrics.graphite.GraphiteReporter; 6 | import com.codahale.metrics.graphite.GraphiteSender; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | 13 | import java.net.InetSocketAddress; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | @Configuration 17 | @Profile("graphite") 18 | class Dashboard { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(Dashboard.class); 21 | 22 | @Bean 23 | public GraphiteReporter graphiteReporter(MetricRegistry metricRegistry) { 24 | final GraphiteReporter reporter = GraphiteReporter 25 | .forRegistry(metricRegistry) 26 | .build(graphite()); 27 | reporter.start(1, TimeUnit.SECONDS); 28 | return reporter; 29 | } 30 | 31 | @Bean 32 | GraphiteSender graphite() { 33 | InetSocketAddress address = new InetSocketAddress("localhost", 2003); 34 | log.info("Connecting to {}", address); 35 | return new Graphite(address); 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/QueryParams.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.util.Random; 6 | 7 | public class QueryParams { 8 | 9 | private static final Random random = new Random(); 10 | 11 | private long duration; 12 | private double standardDev; 13 | private double errorRate; 14 | 15 | public double randomDuration() { 16 | return this.duration + random.nextGaussian() * this.standardDev; 17 | } 18 | 19 | public static Random getRandom() { 20 | return random; 21 | } 22 | 23 | public long getDuration() { 24 | return duration; 25 | } 26 | 27 | public void setDuration(long duration) { 28 | this.duration = duration; 29 | } 30 | 31 | public double getStandardDev() { 32 | return standardDev; 33 | } 34 | 35 | public void setStandardDev(double standardDev) { 36 | this.standardDev = standardDev; 37 | } 38 | 39 | public double getErrorRate() { 40 | return errorRate; 41 | } 42 | 43 | public void setErrorRate(double errorRate) { 44 | this.errorRate = errorRate; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return MoreObjects.toStringHelper(this) 50 | .add("duration", duration) 51 | .add("standardDev", standardDev) 52 | .add("errorRate", errorRate) 53 | .toString(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H23_HystrixGuard.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import com.netflix.hystrix.HystrixCommandKey 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.web.bind.annotation.RequestMapping 8 | import org.springframework.web.bind.annotation.RequestMethod 9 | import org.springframework.web.bind.annotation.RestController 10 | 11 | @RestController 12 | @RequestMapping(method = RequestMethod.GET) 13 | class H23_HystrixGuard { 14 | 15 | @Autowired 16 | private ExternalService externalService 17 | 18 | @RequestMapping("/unsafe") 19 | String unsafe(QueryParams params) { 20 | externalService.call(params) 21 | return "OK" 22 | } 23 | 24 | @RequestMapping("/safe") 25 | String safe(QueryParams params) { 26 | return new HystrixCommand(setter()) { 27 | @Override 28 | protected String run() throws Exception { 29 | externalService.call(params) 30 | return "OK" 31 | } 32 | }.execute() 33 | } 34 | 35 | private HystrixCommand.Setter setter() { 36 | HystrixCommand.Setter.withGroupKey( 37 | HystrixCommandGroupKey.Factory.asKey("External")) 38 | .andCommandKey(HystrixCommandKey.Factory.asKey("/safe")) 39 | } 40 | 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H22_Fallback.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import org.apache.commons.io.IOUtils 6 | import spock.lang.Specification 7 | 8 | import java.nio.charset.StandardCharsets 9 | 10 | import static com.nurkiewicz.hystrix.H22_Fallback.FallbackDownloadCommand.FALLBACK 11 | 12 | /** 13 | * Exceptions are swallowed 14 | */ 15 | class H22_Fallback extends Specification { 16 | 17 | def 'Fallback command'() { 18 | given: 19 | FallbackDownloadCommand command = new FallbackDownloadCommand() 20 | when: 21 | String result = command.execute() 22 | then: 23 | result == FALLBACK 24 | } 25 | 26 | static class FallbackDownloadCommand extends HystrixCommand { 27 | 28 | public static final String FALLBACK = "Temporarily unavailable" 29 | 30 | protected FallbackDownloadCommand() { 31 | super(HystrixCommandGroupKey.Factory.asKey("Download")) 32 | } 33 | 34 | @Override 35 | protected String run() throws Exception { 36 | URL url = "http://www.example.com/404".toURL() 37 | InputStream input = url.openStream() 38 | IOUtils.toString(input, StandardCharsets.UTF_8) 39 | } 40 | 41 | @Override 42 | protected String getFallback() { 43 | return FALLBACK 44 | } 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/ExternalService.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.concurrent.Semaphore; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | @Service 11 | class ExternalService { 12 | 13 | private static final Logger log = LoggerFactory.getLogger(ExternalService.class); 14 | 15 | private final Semaphore throttler = new Semaphore(10); 16 | 17 | String call(QueryParams params) { 18 | throttlerAcquire(); 19 | try { 20 | return businessLogic(params); 21 | } finally { 22 | throttler.release(); 23 | } 24 | } 25 | 26 | private void throttlerAcquire() { 27 | try { 28 | throttler.acquire(); 29 | } catch (InterruptedException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | private String businessLogic(QueryParams params) { 35 | log.debug("Calling with {}", params); 36 | sleepRandomly(params); 37 | if (Math.random() < params.getErrorRate()) { 38 | throw new RuntimeException("Simulated, don't panic"); 39 | } 40 | return "OK"; 41 | } 42 | 43 | private void sleepRandomly(QueryParams params) { 44 | double realDuration = params.randomDuration(); 45 | try { 46 | TimeUnit.MILLISECONDS.sleep((long) realDuration); 47 | } catch (InterruptedException ignored) { 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockController.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.netflix.hystrix.HystrixExecutable; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.math.BigDecimal; 11 | 12 | @RestController 13 | @RequestMapping(method = RequestMethod.GET, value = "/stock") 14 | public class StockController { 15 | 16 | private final StockPriceGateway gateway; 17 | 18 | @Autowired 19 | public StockController(StockPriceGateway gateway) { 20 | this.gateway = gateway; 21 | } 22 | 23 | @RequestMapping("/fast/{ticker}") 24 | public BigDecimal fast(@PathVariable("ticker") String symbol) { 25 | final Ticker ticker = new Ticker(symbol); 26 | final HystrixExecutable cmd = new StockPriceCommand(gateway, ticker); 27 | return cmd.execute().getPrice(); 28 | } 29 | 30 | @RequestMapping("/batch/{ticker}") 31 | public BigDecimal batch(@PathVariable("ticker") String symbol) { 32 | final Ticker ticker = new Ticker(symbol); 33 | final HystrixExecutable cmd = new StockTickerPriceCollapsedCommand(gateway, ticker); 34 | return cmd.execute().getPrice(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/ThrottlingController.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixThreadPoolProperties; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping(method = RequestMethod.GET) 13 | class ThrottlingController { 14 | 15 | @Autowired 16 | ExternalService externalService; 17 | 18 | @RequestMapping("/throttling") 19 | String throttle(QueryParams params) { 20 | final HystrixCommand.Setter key = HystrixCommand.Setter 21 | .withGroupKey(HystrixCommandGroupKey.Factory.asKey("Download")) 22 | .andThreadPoolPropertiesDefaults( 23 | HystrixThreadPoolProperties.Setter() 24 | .withCoreSize(4) 25 | .withMaxQueueSize(20) 26 | .withQueueSizeRejectionThreshold(15) 27 | .withMetricsRollingStatisticalWindowInMilliseconds(15_000) 28 | ); 29 | 30 | final HystrixCommand cmd = new HystrixCommand(key) { 31 | 32 | @Override 33 | protected String run() throws Exception { 34 | return externalService.call(params); 35 | } 36 | }; 37 | return cmd.execute(); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H80_Observables.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import rx.Observable 5 | import spock.lang.Specification 6 | 7 | 8 | class H80_Observables extends Specification { 9 | 10 | def 'asynchronous command'() { 11 | given: 12 | HystrixCommand command = new CustomDownloadCommand("http://www.example.com".toURL()) 13 | when: 14 | Observable obs = command.observe() 15 | then: 16 | Observable result = obs.map { x -> x.length() } 17 | result.toBlocking().single() == 42 18 | } 19 | 20 | def 'compose asynchronous commands'() { 21 | given: 22 | CustomDownloadCommand example = new CustomDownloadCommand("http://www.example.com".toURL()) 23 | CustomDownloadCommand bing = new CustomDownloadCommand("http://www.bing.com".toURL()) 24 | CustomDownloadCommand nurkiewicz = new CustomDownloadCommand("http://www.nurkiewicz.com".toURL()) 25 | and: 26 | Observable exampleObs = example.observe() 27 | Observable bingObs = bing.observe() 28 | Observable nurkiewiczObs = nurkiewicz.observe() 29 | when: 30 | Observable allResults = Observable.merge(exampleObs, bingObs, nurkiewiczObs) 31 | then: 32 | Observable> resultsList = allResults 33 | .filter{html -> html.contains('2015')} 34 | .toList() 35 | resultsList.toBlocking().forEach{println(it)} 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockPrice.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.Instant; 7 | 8 | public class StockPrice { 9 | private final BigDecimal price; 10 | private final Instant effectiveTime; 11 | 12 | public StockPrice(BigDecimal price, Instant effectiveTime) { 13 | this.price = price; 14 | this.effectiveTime = effectiveTime; 15 | } 16 | 17 | public BigDecimal getPrice() { 18 | return price; 19 | } 20 | 21 | public Instant getEffectiveTime() { 22 | return effectiveTime; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return MoreObjects.toStringHelper(this) 28 | .add("price", price) 29 | .add("effectiveTime", effectiveTime) 30 | .toString(); 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) return true; 36 | if (!(o instanceof StockPrice)) return false; 37 | 38 | StockPrice that = (StockPrice) o; 39 | 40 | if (price != null ? !price.equals(that.price) : that.price != null) return false; 41 | return !(effectiveTime != null ? !effectiveTime.equals(that.effectiveTime) : that.effectiveTime != null); 42 | 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | int result = price != null ? price.hashCode() : 0; 48 | result = 31 * result + (effectiveTime != null ? effectiveTime.hashCode() : 0); 49 | return result; 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H12_Nines.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import org.apache.commons.lang.time.DurationFormatUtils 4 | import spock.lang.Specification 5 | import spock.lang.Unroll 6 | 7 | import java.time.Duration 8 | 9 | import static org.apache.commons.lang.time.DateUtils.MILLIS_PER_DAY 10 | 11 | /** 12 | * Downtime grows exponentially with independent services 13 | */ 14 | @Unroll 15 | class H12_Nines extends Specification { 16 | 17 | def 'Uptime of 99.99% when #serviceCount services means #downtimeHms of downtime daily'() { 18 | given: 19 | double independentUptime = 99.99 / 100 20 | double systemUptime = Math.pow(independentUptime, serviceCount) 21 | 22 | expect: 23 | systemUptime * 100 <= expectedMaxUptime 24 | secondsDowntimeDaily(systemUptime) <= expectedDowntimeSecondsDaily 25 | 26 | where: 27 | serviceCount | expectedMaxUptime | expectedDowntimeSecondsDaily 28 | 1 | 99.99 | 8 29 | 2 | 99.99 | 17 30 | 5 | 99.96 | 43 31 | 10 | 99.91 | 86 32 | 20 | 99.81 | 172 33 | 50 | 99.51 | 430 34 | 100 | 99.01 | 859 35 | downtimeHms = Duration.ofSeconds(expectedDowntimeSecondsDaily).toString() 36 | } 37 | 38 | private long secondsDowntimeDaily(double uptime) { 39 | double downtime = 1 - uptime 40 | return downtime * MILLIS_PER_DAY / 1000 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/CircuitController.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix; 2 | 3 | import com.netflix.hystrix.HystrixCommand; 4 | import com.netflix.hystrix.HystrixCommandGroupKey; 5 | import com.netflix.hystrix.HystrixCommandKey; 6 | import com.netflix.hystrix.HystrixCommandProperties; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping(method = RequestMethod.GET) 14 | class CircuitController { 15 | 16 | @Autowired 17 | ExternalService externalService; 18 | 19 | @RequestMapping("/circuit") 20 | String circuit(QueryParams params) { 21 | HystrixCommand.Setter key = HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("Download")) 22 | .andCommandKey(HystrixCommandKey.Factory.asKey("SomeCommand")) 23 | .andCommandPropertiesDefaults( 24 | HystrixCommandProperties.Setter() 25 | .withCircuitBreakerEnabled(true) 26 | .withCircuitBreakerErrorThresholdPercentage(50) 27 | .withCircuitBreakerRequestVolumeThreshold(20) 28 | .withCircuitBreakerSleepWindowInMilliseconds(5_000)); 29 | 30 | final HystrixCommand cmd = new HystrixCommand(key) { 31 | 32 | @Override 33 | protected String run() throws Exception { 34 | return externalService.call(params); 35 | } 36 | }; 37 | return cmd.execute(); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H60_Request_caching.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext 6 | import spock.lang.Specification 7 | 8 | import java.util.concurrent.Future 9 | import java.util.concurrent.TimeUnit 10 | import java.util.concurrent.atomic.LongAdder 11 | 12 | /** 13 | * Two commands aren't executed synchronously 14 | */ 15 | class H60_Request_caching extends Specification { 16 | 17 | def setup() { 18 | HystrixRequestContext.initializeContext() 19 | } 20 | 21 | def cleanup() { 22 | HystrixRequestContext.contextForCurrentThread.shutdown() 23 | } 24 | 25 | def 'should invoke cached action just once'() { 26 | given: 27 | CachedCommand one = new CachedCommand() 28 | CachedCommand two = new CachedCommand() 29 | when: 30 | Future oneObs = one.queue() 31 | Future twoObs = two.queue() 32 | and: 33 | oneObs.get() 34 | twoObs.get() 35 | then: 36 | CachedCommand.counter.intValue() == 1 37 | } 38 | 39 | class CachedCommand extends HystrixCommand { 40 | 41 | public static final LongAdder counter = new LongAdder() 42 | 43 | protected CachedCommand() { 44 | super(HystrixCommandGroupKey.Factory.asKey("Cached")) 45 | } 46 | 47 | @Override 48 | protected String run() throws Exception { 49 | counter.increment() 50 | TimeUnit.MILLISECONDS.sleep(500) 51 | return null 52 | } 53 | 54 | @Override 55 | protected String getCacheKey() { 56 | return "1" 57 | } 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/StockTickerPriceCollapsedCommand.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.netflix.hystrix.HystrixCollapser; 4 | import com.netflix.hystrix.HystrixCollapserKey; 5 | import com.netflix.hystrix.HystrixCollapserProperties; 6 | import com.netflix.hystrix.HystrixCommand; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static java.util.stream.Collectors.toList; 13 | 14 | public class StockTickerPriceCollapsedCommand extends HystrixCollapser, StockPrice, Ticker> { 15 | 16 | private final StockPriceGateway gateway; 17 | private final Ticker stock; 18 | 19 | public StockTickerPriceCollapsedCommand(StockPriceGateway gateway, Ticker stock) { 20 | super( 21 | HystrixCollapser.Setter.withCollapserKey( 22 | HystrixCollapserKey.Factory.asKey("Stock")) 23 | .andCollapserPropertiesDefaults( 24 | HystrixCollapserProperties.Setter() 25 | .withTimerDelayInMilliseconds(100) 26 | .withMaxRequestsInBatch(500) 27 | ) 28 | .andScope(Scope.GLOBAL)); 29 | this.gateway = gateway; 30 | this.stock = stock; 31 | } 32 | 33 | @Override 34 | public Ticker getRequestArgument() { 35 | return stock; 36 | } 37 | 38 | @Override 39 | protected HystrixCommand> createCommand(Collection> collapsedRequests) { 40 | final List stocks = collapsedRequests.stream() 41 | .map(CollapsedRequest::getArgument) 42 | .collect(toList()); 43 | return new StockPricesBatchCommand(gateway, stocks); 44 | } 45 | 46 | @Override 47 | protected void mapResponseToRequests(Map batchResponse, Collection> collapsedRequests) { 48 | collapsedRequests.forEach(request -> { 49 | final Ticker ticker = request.getArgument(); 50 | final StockPrice price = batchResponse.get(ticker); 51 | request.setResponse(price); 52 | }); 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H30_Circuit_breaker.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.netflix.hystrix.HystrixCommand 4 | import com.netflix.hystrix.HystrixCommandGroupKey 5 | import com.netflix.hystrix.HystrixCommandKey 6 | import com.netflix.hystrix.HystrixCommandProperties 7 | import com.netflix.hystrix.HystrixThreadPoolProperties 8 | import groovy.util.logging.Slf4j 9 | import org.apache.commons.io.IOUtils 10 | import spock.lang.Specification 11 | import com.netflix.hystrix.HystrixCommand.Setter 12 | import java.nio.charset.StandardCharsets 13 | 14 | /** 15 | * Circuit breaker 16 | * - Enabled by default 17 | * - Configurable volume threshold 18 | * - Configurable error percentage 19 | * - Configurable window to make one testing attempt 20 | * @see com.nurkiewicz.hystrix.CircuitController 21 | */ 22 | class H30_Circuit_breaker extends Specification { 23 | 24 | def 'Minimal Hystrix API'() { 25 | given: 26 | CircuitBreakingDownloadCommand command = new CircuitBreakingDownloadCommand() 27 | 28 | expect: 29 | String result = command.execute() 30 | } 31 | 32 | @Slf4j 33 | static class CircuitBreakingDownloadCommand extends HystrixCommand { 34 | 35 | protected CircuitBreakingDownloadCommand() { 36 | super( 37 | Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("Download")) 38 | .andCommandKey(HystrixCommandKey.Factory.asKey("SomeCommand")) 39 | .andThreadPoolPropertiesDefaults( 40 | HystrixThreadPoolProperties.Setter() 41 | .withMetricsRollingStatisticalWindowInMilliseconds(10_000)) 42 | .andCommandPropertiesDefaults( 43 | HystrixCommandProperties.Setter() 44 | .withCircuitBreakerEnabled(true) 45 | .withCircuitBreakerErrorThresholdPercentage(50) 46 | .withCircuitBreakerRequestVolumeThreshold(20) 47 | .withCircuitBreakerSleepWindowInMilliseconds(5_000) 48 | ) 49 | ) 50 | } 51 | 52 | @Override 53 | protected String run() throws Exception { 54 | log.debug("Downloading...") 55 | URL url = "http://www.example.com/404".toURL() 56 | InputStream input = url.openStream() 57 | return IOUtils.toString(input, StandardCharsets.UTF_8) 58 | } 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/MainApplication.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.codahale.metrics.JmxReporter 4 | import com.codahale.metrics.MetricRegistry 5 | import com.netflix.hystrix.contrib.codahalemetricspublisher.HystrixCodaHaleMetricsPublisher 6 | import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet 7 | import com.netflix.hystrix.contrib.requestservlet.HystrixRequestContextServletFilter 8 | import com.netflix.hystrix.strategy.HystrixPlugins 9 | import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.boot.SpringApplication 12 | import org.springframework.boot.autoconfigure.SpringBootApplication 13 | import org.springframework.boot.context.embedded.FilterRegistrationBean 14 | import org.springframework.boot.context.embedded.ServletRegistrationBean 15 | import org.springframework.context.annotation.Bean 16 | 17 | @SpringBootApplication 18 | class MainApplication { 19 | 20 | @Autowired 21 | private ExternalService externalService 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(MainApplication, args) 25 | } 26 | 27 | @Bean 28 | public ServletRegistrationBean statisticsStream() { 29 | return new ServletRegistrationBean(hystrixServlet(), "/hystrix.stream") 30 | } 31 | 32 | @Bean 33 | public FilterRegistrationBean hystrixRequestContextServletFilterRegistrationBean() { 34 | final FilterRegistrationBean filterBean = new FilterRegistrationBean() 35 | filterBean.setFilter(hystrixRequestContextServletFilter()) 36 | filterBean.setUrlPatterns(["/*"]) 37 | return filterBean 38 | } 39 | 40 | @Bean 41 | public HystrixRequestContextServletFilter hystrixRequestContextServletFilter() { 42 | new HystrixRequestContextServletFilter() 43 | } 44 | 45 | @Bean 46 | public HystrixMetricsStreamServlet hystrixServlet() { 47 | new HystrixMetricsStreamServlet() 48 | } 49 | 50 | @Bean 51 | HystrixMetricsPublisher hystrixMetricsPublisher(MetricRegistry metricRegistry) { 52 | HystrixCodaHaleMetricsPublisher publisher = new HystrixCodaHaleMetricsPublisher(metricRegistry); 53 | HystrixPlugins.getInstance().registerMetricsPublisher(publisher); 54 | final JmxReporter reporter = JmxReporter.forRegistry(metricRegistry).build(); 55 | reporter.start(); 56 | return publisher; 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/test/java/com/nurkiewicz/hystrix/stock/SimulatedStockPriceGateway.java: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix.stock; 2 | 3 | import com.codahale.metrics.Counter; 4 | import com.codahale.metrics.Histogram; 5 | import com.codahale.metrics.MetricRegistry; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.math.BigDecimal; 12 | import java.time.Instant; 13 | import java.util.*; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | import static java.util.stream.Collectors.toMap; 17 | 18 | @Service 19 | class SimulatedStockPriceGateway implements StockPriceGateway { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(SimulatedStockPriceGateway.class); 22 | private static Random RAND = new Random(); 23 | private final Counter requests; 24 | private final Histogram requestSize; 25 | private final Histogram inputSize; 26 | 27 | @Autowired 28 | public SimulatedStockPriceGateway(MetricRegistry metricRegistry) { 29 | requests = metricRegistry.counter(SimulatedStockPriceGateway.class.getSimpleName() + ".requests"); 30 | inputSize = metricRegistry.histogram(SimulatedStockPriceGateway.class.getSimpleName() + ".inputSize"); 31 | requestSize = metricRegistry.histogram(SimulatedStockPriceGateway.class.getSimpleName() + ".requestSize"); 32 | } 33 | 34 | @Override 35 | public StockPrice load(Ticker ticker) { 36 | log.debug("Loading {}", ticker); 37 | final Set oneTicker = Collections.singleton(ticker); 38 | return loadMany(oneTicker).get(ticker); 39 | } 40 | 41 | @Override 42 | public Map loadMany(Collection tickers) { 43 | final HashSet uniqueTickers = new HashSet<>(tickers); 44 | log.debug("Loading batch of ({}, unique: {}): {}", tickers.size(), uniqueTickers.size(), uniqueTickers); 45 | requests.inc(); 46 | inputSize.update(tickers.size()); 47 | requestSize.update(uniqueTickers.size()); 48 | randomArtificialSleep(uniqueTickers.size()); 49 | return uniqueTickers 50 | .stream() 51 | .collect(toMap(t -> t, t -> new StockPrice(BigDecimal.ONE, Instant.now()))); 52 | } 53 | 54 | private static void randomArtificialSleep(int dataSize) { 55 | try { 56 | final int millis = 10 + RAND.nextInt(10 * dataSize); 57 | TimeUnit.MILLISECONDS.sleep(millis); 58 | } catch (InterruptedException ignored) { 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/groovy/com/nurkiewicz/hystrix/H61_Request_batching.groovy: -------------------------------------------------------------------------------- 1 | package com.nurkiewicz.hystrix 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext 5 | import com.nurkiewicz.hystrix.stock.StockPrice 6 | import com.nurkiewicz.hystrix.stock.StockPriceGateway 7 | import com.nurkiewicz.hystrix.stock.StockTickerPriceCollapsedCommand 8 | import com.nurkiewicz.hystrix.stock.Ticker 9 | import spock.lang.Specification 10 | 11 | import java.time.Instant 12 | import java.util.concurrent.Future 13 | 14 | import static com.nurkiewicz.hystrix.H61_Request_batching.Examples.* 15 | 16 | /** 17 | * Stock price example 18 | * @see com.nurkiewicz.hystrix.stock.StockPriceCommand first 19 | */ 20 | class H61_Request_batching extends Specification { 21 | def setup() { 22 | HystrixRequestContext.initializeContext() 23 | } 24 | 25 | def cleanup() { 26 | HystrixRequestContext.contextForCurrentThread.shutdown() 27 | } 28 | 29 | def 'should collapse two commands executed concurrently for the same stock ticker'() { 30 | given: 31 | def gateway = Mock(StockPriceGateway) 32 | def tickers = [ANY_TICKER] as Set 33 | 34 | and: 35 | def commandOne = new StockTickerPriceCollapsedCommand(gateway, ANY_TICKER) 36 | def commandTwo = new StockTickerPriceCollapsedCommand(gateway, ANY_TICKER) 37 | 38 | when: 39 | Future futureOne = commandOne.queue() 40 | Future futureTwo = commandTwo.queue() 41 | 42 | and: 43 | futureOne.get() 44 | futureTwo.get() 45 | 46 | then: 47 | 0 * gateway.load(_) 48 | 1 * gateway.loadMany(tickers) >> ImmutableMap.of(ANY_TICKER, ANY_STOCK_PRICE) 49 | } 50 | 51 | def 'should collapse two commands executed concurrently for the different stock tickers'() { 52 | given: 53 | def gateway = Mock(StockPriceGateway) 54 | def tickers = [ANY_TICKER, OTHER_TICKER] as Set 55 | 56 | and: 57 | def commandOne = new StockTickerPriceCollapsedCommand(gateway, ANY_TICKER) 58 | def commandTwo = new StockTickerPriceCollapsedCommand(gateway, OTHER_TICKER) 59 | 60 | when: 61 | Future futureOne = commandOne.queue() 62 | Future futureTwo = commandTwo.queue() 63 | 64 | and: 65 | futureOne.get() 66 | futureTwo.get() 67 | 68 | then: 69 | 1 * gateway.loadMany(tickers) >> ImmutableMap.of( 70 | ANY_TICKER, ANY_STOCK_PRICE, 71 | OTHER_TICKER, OTHER_STOCK_PRICE) 72 | } 73 | 74 | class Examples { 75 | 76 | static final Ticker ANY_TICKER = new Ticker("IBM") 77 | static final StockPrice ANY_STOCK_PRICE = new StockPrice(BigDecimal.TEN, Instant.now()) 78 | 79 | static final Ticker OTHER_TICKER = new Ticker("MSFT") 80 | static final StockPrice OTHER_STOCK_PRICE = new StockPrice(BigDecimal.ONE, Instant.now()) 81 | 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.nurkiewicz.hystrix 5 | hystrix 6 | 0.0.1-SNAPSHOT 7 | 8 | UTF-8 9 | 4.1.1.RELEASE 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 1.2.3.RELEASE 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 3.0 23 | 24 | 1.8 25 | 1.8 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | com.netflix.hystrix 34 | hystrix-core 35 | 1.4.18 36 | 37 | 38 | com.netflix.hystrix 39 | hystrix-request-servlet 40 | 1.4.18 41 | 42 | 43 | com.netflix.hystrix 44 | hystrix-metrics-event-stream 45 | 1.4.18 46 | 47 | 48 | com.netflix.hystrix 49 | hystrix-codahale-metrics-publisher 50 | 1.4.18 51 | 52 | 53 | com.codahale.metrics 54 | metrics-core 55 | 56 | 57 | 58 | 59 | io.dropwizard.metrics 60 | metrics-core 61 | 3.1.0 62 | 63 | 64 | io.dropwizard.metrics 65 | metrics-graphite 66 | 3.1.0 67 | 68 | 69 | commons-io 70 | commons-io 71 | 2.4 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-web 76 | 1.2.3.RELEASE 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-actuator 81 | 1.2.3.RELEASE 82 | 83 | 84 | org.spockframework 85 | spock-core 86 | 1.0-groovy-2.4 87 | test 88 | 89 | 90 | com.google.guava 91 | guava 92 | 18.0 93 | 94 | 95 | cglib 96 | cglib-nodep 97 | 3.1 98 | 99 | 100 | org.springframework 101 | spring-beans 102 | 4.1.6.RELEASE 103 | 104 | 105 | org.springframework 106 | spring-context 107 | 4.1.6.RELEASE 108 | 109 | 110 | org.springframework 111 | spring-context-support 112 | 4.1.6.RELEASE 113 | 114 | 115 | org.springframework 116 | spring-webmvc 117 | 4.1.6.RELEASE 118 | 119 | 120 | org.springframework 121 | spring-web 122 | 4.1.6.RELEASE 123 | 124 | 125 | org.springframework 126 | spring-aop 127 | 4.1.6.RELEASE 128 | 129 | 130 | org.springframework 131 | spring-core 132 | 4.1.6.RELEASE 133 | 134 | 135 | org.springframework 136 | spring-expression 137 | 4.1.6.RELEASE 138 | 139 | 140 | org.codehaus.groovy 141 | groovy-all 142 | 2.4.3 143 | 144 | 145 | 146 | 147 | --------------------------------------------------------------------------------