/** * Copyright 2015 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; import com.hystrix.junit.HystrixRequestContextRule; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import com.netflix.hystrix.strategy.concurrency.HystrixContextScheduler; import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; import com.netflix.hystrix.strategy.properties.HystrixPropertiesCollapserDefault; import com.netflix.hystrix.strategy.properties.HystrixPropertiesFactory; import com.netflix.hystrix.util.HystrixTimer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import com.netflix.hystrix.HystrixCollapser.CollapsedRequest; import com.netflix.hystrix.collapser.CollapserTimer; import com.netflix.hystrix.collapser.RealCollapserTimer; import com.netflix.hystrix.collapser.RequestCollapser; import com.netflix.hystrix.collapser.RequestCollapserFactory; import com.netflix.hystrix.strategy.HystrixPlugins; import com.netflix.hystrix.strategy.concurrency.HystrixContextRunnable; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableHolder; import com.netflix.hystrix.util.HystrixTimer.TimerListener; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; import rx.observers.Subscribers; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import static org.junit.Assert.*; public class HystrixCollapserTest { @Rule public HystrixRequestContextRule context = new HystrixRequestContextRule(); @Before public void init() { HystrixCollapserMetrics.reset(); HystrixCommandMetrics.reset(); HystrixPropertiesFactory.reset(); } @Test public void testTwoRequests() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Future<String> response1 = collapser1.queue(); HystrixCollapser<List<String>, String, String> collapser2 = new TestRequestCollapser(timer, 2); Future<String> response2 = collapser2.queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1", response1.get()); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixCollapserMetrics metrics = collapser1.getMetrics(); assertSame(metrics, collapser2.getMetrics()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertEquals(2, command.getNumberCollapsed()); } @Test public void testMultipleBatches() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestRequestCollapser(timer, 2).queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); // now request more Future<String> response3 = new TestRequestCollapser(timer, 3).queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); // we should have had it execute twice now assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } @Test public void testMaxRequestsInBatch() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1, 2, 10); HystrixCollapser<List<String>, String, String> collapser2 = new TestRequestCollapser(timer, 2, 2, 10); HystrixCollapser<List<String>, String, String> collapser3 = new TestRequestCollapser(timer, 3, 2, 10); System.out.println("*** " + System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " Constructed the collapsers"); Future<String> response1 = collapser1.queue(); Future<String> response2 = collapser2.queue(); Future<String> response3 = collapser3.queue(); System.out.println("*** " +System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " queued the collapsers"); timer.incrementTime(10); // let time pass that equals the default delay/period System.out.println("*** " +System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " incremented the virtual timer"); assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); // we should have had it execute twice because the batch size was 2 System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } @Test public void testRequestsOverTime() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Future<String> response1 = collapser1.queue(); timer.incrementTime(5); Future<String> response2 = new TestRequestCollapser(timer, 2).queue(); timer.incrementTime(8); // should execute here Future<String> response3 = new TestRequestCollapser(timer, 3).queue(); timer.incrementTime(6); Future<String> response4 = new TestRequestCollapser(timer, 4).queue(); timer.incrementTime(8); // should execute here Future<String> response5 = new TestRequestCollapser(timer, 5).queue(); timer.incrementTime(10); // should execute here // wait for all tasks to complete assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4", response4.get(1000, TimeUnit.MILLISECONDS)); assertEquals("5", response5.get(1000, TimeUnit.MILLISECONDS)); assertEquals(3, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } class Pair<A, B> { final A a; final B b; Pair(A a, B b) { this.a = a; this.b = b; } } class MyCommand extends HystrixCommand<List<Pair<String, Integer>>> { private final List<String> args; public MyCommand(List<String> args) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BATCH"))); this.args = args; } @Override protected List<Pair<String, Integer>> run() throws Exception { System.out.println("Executing batch command on : " + Thread.currentThread().getName() + " with args : " + args); List<Pair<String, Integer>> results = new ArrayList<Pair<String, Integer>>(); for (String arg: args) { results.add(new Pair<String, Integer>(arg, Integer.parseInt(arg))); } return results; } } class MyCollapser extends HystrixCollapser<List<Pair<String, Integer>>, Integer, String> { private final String arg; MyCollapser(String arg, boolean reqCacheEnabled) { super(HystrixCollapserKey.Factory.asKey("UNITTEST"), Scope.REQUEST, new RealCollapserTimer(), HystrixCollapserProperties.Setter().withRequestCacheEnabled(reqCacheEnabled), HystrixCollapserMetrics.getInstance(HystrixCollapserKey.Factory.asKey("UNITTEST"), new HystrixPropertiesCollapserDefault(HystrixCollapserKey.Factory.asKey("UNITTEST"), HystrixCollapserProperties.Setter()))); this.arg = arg; } @Override public String getRequestArgument() { return arg; } @Override protected HystrixCommand<List<Pair<String, Integer>>> createCommand(Collection<CollapsedRequest<Integer, String>> collapsedRequests) { List<String> args = new ArrayList<String>(collapsedRequests.size()); for (CollapsedRequest<Integer, String> req: collapsedRequests) { args.add(req.getArgument()); } return new MyCommand(args); } @Override protected void mapResponseToRequests(List<Pair<String, Integer>> batchResponse, Collection<CollapsedRequest<Integer, String>> collapsedRequests) { for (Pair<String, Integer> pair: batchResponse) { for (CollapsedRequest<Integer, String> collapsedReq: collapsedRequests) { if (collapsedReq.getArgument().equals(pair.a)) { collapsedReq.setResponse(pair.b); } } } } @Override protected String getCacheKey() { return arg; } } @Test public void testDuplicateArgumentsWithRequestCachingOn() throws Exception { final int NUM = 10; List<Observable<Integer>> observables = new ArrayList<Observable<Integer>>(); for (int i = 0; i < NUM; i++) { MyCollapser c = new MyCollapser("5", true); observables.add(c.toObservable()); } List<TestSubscriber<Integer>> subscribers = new ArrayList<TestSubscriber<Integer>>(); for (final Observable<Integer> o: observables) { final TestSubscriber<Integer> sub = new TestSubscriber<Integer>(); subscribers.add(sub); o.subscribe(sub); } Thread.sleep(100); //all subscribers should receive the same value for (TestSubscriber<Integer> sub: subscribers) { sub.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); System.out.println("Subscriber received : " + sub.getOnNextEvents()); sub.assertNoErrors(); sub.assertValues(5); } } @Test public void testDuplicateArgumentsWithRequestCachingOff() throws Exception { final int NUM = 10; List<Observable<Integer>> observables = new ArrayList<Observable<Integer>>(); for (int i = 0; i < NUM; i++) { MyCollapser c = new MyCollapser("5", false); observables.add(c.toObservable()); } List<TestSubscriber<Integer>> subscribers = new ArrayList<TestSubscriber<Integer>>(); for (final Observable<Integer> o: observables) { final TestSubscriber<Integer> sub = new TestSubscriber<Integer>(); subscribers.add(sub); o.subscribe(sub); } Thread.sleep(100); AtomicInteger numErrors = new AtomicInteger(0); AtomicInteger numValues = new AtomicInteger(0); // only the first subscriber should receive the value. // the others should get an error that the batch contains duplicates for (TestSubscriber<Integer> sub: subscribers) { sub.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); if (sub.getOnCompletedEvents().isEmpty()) { System.out.println(Thread.currentThread().getName() + " Error : " + sub.getOnErrorEvents()); sub.assertError(IllegalArgumentException.class); sub.assertNoValues(); numErrors.getAndIncrement(); } else { System.out.println(Thread.currentThread().getName() + " OnNext : " + sub.getOnNextEvents()); sub.assertValues(5); sub.assertCompleted(); sub.assertNoErrors(); numValues.getAndIncrement(); } } assertEquals(1, numValues.get()); assertEquals(NUM - 1, numErrors.get()); } @Test public void testUnsubscribeFromSomeDuplicateArgsDoesNotRemoveFromBatch() throws Exception { final int NUM = 10; List<Observable<Integer>> observables = new ArrayList<Observable<Integer>>(); for (int i = 0; i < NUM; i++) { MyCollapser c = new MyCollapser("5", true); observables.add(c.toObservable()); } List<TestSubscriber<Integer>> subscribers = new ArrayList<TestSubscriber<Integer>>(); List<Subscription> subscriptions = new ArrayList<Subscription>(); for (final Observable<Integer> o: observables) { final TestSubscriber<Integer> sub = new TestSubscriber<Integer>(); subscribers.add(sub); Subscription s = o.subscribe(sub); subscriptions.add(s); } //unsubscribe from all but 1 for (int i = 0; i < NUM - 1; i++) { Subscription s = subscriptions.get(i); s.unsubscribe(); } Thread.sleep(100); //all subscribers with an active subscription should receive the same value for (TestSubscriber<Integer> sub: subscribers) { if (!sub.isUnsubscribed()) { sub.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS); System.out.println("Subscriber received : " + sub.getOnNextEvents()); sub.assertNoErrors(); sub.assertValues(5); } else { System.out.println("Subscriber is unsubscribed"); } } } @Test public void testUnsubscribeOnOneDoesntKillBatch() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestRequestCollapser(timer, 2).queue(); // kill the first response1.cancel(true); timer.incrementTime(10); // let time pass that equals the default delay/period // the first is cancelled so should return null try { response1.get(1000, TimeUnit.MILLISECONDS); fail("expect CancellationException after cancelling"); } catch (CancellationException e) { // expected } // we should still get a response on the second assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } @Test public void testShardedRequests() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestShardedRequestCollapser(timer, "1a"); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestShardedRequestCollapser(timer, "2b").queue(); Future<String> response3 = new TestShardedRequestCollapser(timer, "3b").queue(); Future<String> response4 = new TestShardedRequestCollapser(timer, "4a").queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1a", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2b", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3b", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4a", response4.get(1000, TimeUnit.MILLISECONDS)); /* we should get 2 batches since it gets sharded */ assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } @Test public void testRequestScope() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, "1"); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestRequestCollapser(timer, "2").queue(); // simulate a new request RequestCollapserFactory.resetRequest(); Future<String> response3 = new TestRequestCollapser(timer, "3").queue(); Future<String> response4 = new TestRequestCollapser(timer, "4").queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4", response4.get(1000, TimeUnit.MILLISECONDS)); // 2 different batches should execute, 1 per request assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } @Test public void testGlobalScope() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestGloballyScopedRequestCollapser(timer, "1"); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestGloballyScopedRequestCollapser(timer, "2").queue(); // simulate a new request RequestCollapserFactory.resetRequest(); Future<String> response3 = new TestGloballyScopedRequestCollapser(timer, "3").queue(); Future<String> response4 = new TestGloballyScopedRequestCollapser(timer, "4").queue(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4", response4.get(1000, TimeUnit.MILLISECONDS)); // despite having cleared the cache in between we should have a single execution because this is on the global not request cache assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(4, cmdIterator.next().getNumberCollapsed()); } @Test public void testErrorHandlingViaFutureException() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapserWithFaultyCreateCommand(timer, "1"); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestRequestCollapserWithFaultyCreateCommand(timer, "2").queue(); timer.incrementTime(10); // let time pass that equals the default delay/period try { response1.get(1000, TimeUnit.MILLISECONDS); fail("we should have received an exception"); } catch (ExecutionException e) { // what we expect } try { response2.get(1000, TimeUnit.MILLISECONDS); fail("we should have received an exception"); } catch (ExecutionException e) { // what we expect } assertEquals(0, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); } @Test public void testErrorHandlingWhenMapToResponseFails() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapserWithFaultyMapToResponse(timer, "1"); Future<String> response1 = collapser1.queue(); Future<String> response2 = new TestRequestCollapserWithFaultyMapToResponse(timer, "2").queue(); timer.incrementTime(10); // let time pass that equals the default delay/period try { response1.get(1000, TimeUnit.MILLISECONDS); fail("we should have received an exception"); } catch (ExecutionException e) { // what we expect } try { response2.get(1000, TimeUnit.MILLISECONDS); fail("we should have received an exception"); } catch (ExecutionException e) { // what we expect } // the batch failed so no executions // but it still executed the command once assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } @Test public void testRequestVariableLifecycle1() throws Exception { HystrixRequestContext reqContext = HystrixRequestContext.initializeContext(); // do actual work TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Future<String> response1 = collapser1.queue(); timer.incrementTime(5); Future<String> response2 = new TestRequestCollapser(timer, 2).queue(); timer.incrementTime(8); // should execute here Future<String> response3 = new TestRequestCollapser(timer, 3).queue(); timer.incrementTime(6); Future<String> response4 = new TestRequestCollapser(timer, 4).queue(); timer.incrementTime(8); // should execute here Future<String> response5 = new TestRequestCollapser(timer, 5).queue(); timer.incrementTime(10); // should execute here // wait for all tasks to complete assertEquals("1", response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4", response4.get(1000, TimeUnit.MILLISECONDS)); assertEquals("5", response5.get(1000, TimeUnit.MILLISECONDS)); // each task should have been executed 3 times for (TestCollapserTimer.ATask t : timer.tasks) { assertEquals(3, t.task.count.get()); } System.out.println("timer.tasks.size() A: " + timer.tasks.size()); System.out.println("tasks in test: " + timer.tasks); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(1, cmdIterator.next().getNumberCollapsed()); System.out.println("timer.tasks.size() B: " + timer.tasks.size()); HystrixRequestVariableHolder<RequestCollapser<?, ?, ?>> rv = RequestCollapserFactory.getRequestVariable(new TestRequestCollapser(timer, 1).getCollapserKey().name()); reqContext.close(); assertNotNull(rv); // they should have all been removed as part of ThreadContext.remove() assertEquals(0, timer.tasks.size()); } @Test public void testRequestVariableLifecycle2() throws Exception { final HystrixRequestContext reqContext = HystrixRequestContext.initializeContext(); final TestCollapserTimer timer = new TestCollapserTimer(); final ConcurrentLinkedQueue<Future<String>> responses = new ConcurrentLinkedQueue<Future<String>>(); ConcurrentLinkedQueue<Thread> threads = new ConcurrentLinkedQueue<Thread>(); // kick off work (simulating a single request with multiple threads) for (int t = 0; t < 5; t++) { final int outerLoop = t; Thread th = new Thread(new HystrixContextRunnable(HystrixPlugins.getInstance().getConcurrencyStrategy(), new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { int uniqueInt = (outerLoop * 100) + i; responses.add(new TestRequestCollapser(timer, uniqueInt).queue()); } } })); threads.add(th); th.start(); } for (Thread th : threads) { // wait for each thread to finish th.join(); } // we expect 5 threads * 100 responses each assertEquals(500, responses.size()); for (Future<String> f : responses) { // they should not be done yet because the counter hasn't incremented assertFalse(f.isDone()); } timer.incrementTime(5); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 2); Future<String> response2 = collapser1.queue(); timer.incrementTime(8); // should execute here Future<String> response3 = new TestRequestCollapser(timer, 3).queue(); timer.incrementTime(6); Future<String> response4 = new TestRequestCollapser(timer, 4).queue(); timer.incrementTime(8); // should execute here Future<String> response5 = new TestRequestCollapser(timer, 5).queue(); timer.incrementTime(10); // should execute here // wait for all tasks to complete for (Future<String> f : responses) { f.get(1000, TimeUnit.MILLISECONDS); } assertEquals("2", response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("3", response3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("4", response4.get(1000, TimeUnit.MILLISECONDS)); assertEquals("5", response5.get(1000, TimeUnit.MILLISECONDS)); // each task should have been executed 3 times for (TestCollapserTimer.ATask t : timer.tasks) { assertEquals(3, t.task.count.get()); } Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(500, cmdIterator.next().getNumberCollapsed()); assertEquals(2, cmdIterator.next().getNumberCollapsed()); assertEquals(1, cmdIterator.next().getNumberCollapsed()); HystrixRequestVariableHolder<RequestCollapser<?, ?, ?>> rv = RequestCollapserFactory.getRequestVariable(new TestRequestCollapser(timer, 1).getCollapserKey().name()); reqContext.close(); assertNotNull(rv); // they should have all been removed as part of ThreadContext.remove() assertEquals(0, timer.tasks.size()); } /** * Test Request scoped caching of commands so that a 2nd duplicate call doesn't execute but returns the previous Future */ @Test public void testRequestCache1() { final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "A", true); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "A", true); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("A", f2.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } Future<String> f3 = command1.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f3.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } // we should still have executed only one command assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().toArray(new HystrixInvokableInfo<?>[1])[0]; System.out.println("command.getExecutionEvents(): " + command.getExecutionEvents()); assertEquals(2, command.getExecutionEvents().size()); assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } /** * Test Request scoped caching doesn't prevent different ones from executing */ @Test public void testRequestCache2() { final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "A", true); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "B", true); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f2.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } Future<String> f3 = command1.queue(); Future<String> f4 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f3.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f4.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } // we should still have executed only one command assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().toArray(new HystrixInvokableInfo<?>[1])[0]; assertEquals(2, command.getExecutionEvents().size()); assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } /** * Test Request scoped caching with a mixture of commands */ @Test public void testRequestCache3() { final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "A", true); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "B", true); SuccessfulCacheableCollapsedCommand command3 = new SuccessfulCacheableCollapsedCommand(timer, "B", true); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); Future<String> f3 = command3.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f3.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } Future<String> f4 = command1.queue(); Future<String> f5 = command2.queue(); Future<String> f6 = command3.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f4.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f5.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f6.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } // we should still have executed only one command assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().toArray(new HystrixInvokableInfo<?>[1])[0]; assertEquals(2, command.getExecutionEvents().size()); assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } /** * Test Request scoped caching with a mixture of commands */ @Test public void testNoRequestCache3() { final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "A", false); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "B", false); SuccessfulCacheableCollapsedCommand command3 = new SuccessfulCacheableCollapsedCommand(timer, "B", false); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); Future<String> f3 = command3.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f2.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f3.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } Future<String> f4 = command1.queue(); Future<String> f5 = command2.queue(); Future<String> f6 = command3.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f4.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f5.get(1000, TimeUnit.MILLISECONDS)); assertEquals("B", f6.get(1000, TimeUnit.MILLISECONDS)); } catch (Exception e) { throw new RuntimeException(e); } // request caching is turned off on this so we expect 2 command executions assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); // we expect to see it with SUCCESS and COLLAPSED and both HystrixInvokableInfo<?> commandA = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().toArray(new HystrixInvokableInfo<?>[2])[0]; assertEquals(2, commandA.getExecutionEvents().size()); assertTrue(commandA.getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(commandA.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); // we expect to see it with SUCCESS and COLLAPSED and both HystrixInvokableInfo<?> commandB = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().toArray(new HystrixInvokableInfo<?>[2])[1]; assertEquals(2, commandB.getExecutionEvents().size()); assertTrue(commandB.getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(commandB.getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); //1 for A, 1 for B. Batch contains only unique arguments (no duplicates) assertEquals(2, cmdIterator.next().getNumberCollapsed()); //1 for A, 1 for B. Batch contains only unique arguments (no duplicates) } /** * Test command that uses a null request argument */ @Test public void testRequestCacheWithNullRequestArgument() throws Exception { ConcurrentLinkedQueue<HystrixCommand<List<String>>> commands = new ConcurrentLinkedQueue<HystrixCommand<List<String>>>(); final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, null, true, commands); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, null, true, commands); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); assertEquals("NULL", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("NULL", f2.get(1000, TimeUnit.MILLISECONDS)); // it should have executed 1 command assertEquals(1, commands.size()); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.SUCCESS)); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Future<String> f3 = command1.queue(); // increment past batch time so it executes timer.incrementTime(15); assertEquals("NULL", f3.get(1000, TimeUnit.MILLISECONDS)); // it should still be 1 ... no new executions assertEquals(1, commands.size()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } @Test public void testRequestCacheWithCommandError() { ConcurrentLinkedQueue<HystrixCommand<List<String>>> commands = new ConcurrentLinkedQueue<HystrixCommand<List<String>>>(); final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "FAILURE", true, commands); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "FAILURE", true, commands); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("A", f2.get(1000, TimeUnit.MILLISECONDS)); fail("exception should have been thrown"); } catch (Exception e) { // expected } // it should have executed 1 command assertEquals(1, commands.size()); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.FAILURE)); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Future<String> f3 = command1.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f3.get(1000, TimeUnit.MILLISECONDS)); fail("exception should have been thrown"); } catch (Exception e) { // expected } // it should still be 1 ... no new executions assertEquals(1, commands.size()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } /** * Test that a command that times out will still be cached and when retrieved will re-throw the exception. */ @Test public void testRequestCacheWithCommandTimeout() { ConcurrentLinkedQueue<HystrixCommand<List<String>>> commands = new ConcurrentLinkedQueue<HystrixCommand<List<String>>>(); final TestCollapserTimer timer = new TestCollapserTimer(); SuccessfulCacheableCollapsedCommand command1 = new SuccessfulCacheableCollapsedCommand(timer, "TIMEOUT", true, commands); SuccessfulCacheableCollapsedCommand command2 = new SuccessfulCacheableCollapsedCommand(timer, "TIMEOUT", true, commands); Future<String> f1 = command1.queue(); Future<String> f2 = command2.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f1.get(1000, TimeUnit.MILLISECONDS)); assertEquals("A", f2.get(1000, TimeUnit.MILLISECONDS)); fail("exception should have been thrown"); } catch (Exception e) { // expected } // it should have executed 1 command assertEquals(1, commands.size()); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.TIMEOUT)); assertTrue(commands.peek().getExecutionEvents().contains(HystrixEventType.COLLAPSED)); Future<String> f3 = command1.queue(); // increment past batch time so it executes timer.incrementTime(15); try { assertEquals("A", f3.get(1000, TimeUnit.MILLISECONDS)); fail("exception should have been thrown"); } catch (Exception e) { // expected } // it should still be 1 ... no new executions assertEquals(1, commands.size()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } /** * Test how the collapser behaves when the circuit is short-circuited */ @Test public void testRequestWithCommandShortCircuited() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapserWithShortCircuitedCommand(timer, "1"); Observable<String> response1 = collapser1.observe(); Observable<String> response2 = new TestRequestCollapserWithShortCircuitedCommand(timer, "2").observe(); timer.incrementTime(10); // let time pass that equals the default delay/period try { response1.toBlocking().first(); fail("we should have received an exception"); } catch (Exception e) { e.printStackTrace(); // what we expect } try { response2.toBlocking().first(); fail("we should have received an exception"); } catch (Exception e) { e.printStackTrace(); // what we expect } // it will execute once (short-circuited) assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } /** * Test a Void response type - null being set as response. * * @throws Exception */ @Test public void testVoidResponseTypeFireAndForgetCollapsing1() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); TestCollapserWithVoidResponseType collapser1 = new TestCollapserWithVoidResponseType(timer, 1); Future<Void> response1 = collapser1.queue(); Future<Void> response2 = new TestCollapserWithVoidResponseType(timer, 2).queue(); timer.incrementTime(100); // let time pass that equals the default delay/period // normally someone wouldn't wait on these, but we need to make sure they do in fact return // and not block indefinitely in case someone does call get() assertEquals(null, response1.get(1000, TimeUnit.MILLISECONDS)); assertEquals(null, response2.get(1000, TimeUnit.MILLISECONDS)); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } /** * Test a Void response type - response never being set in mapResponseToRequest * * @throws Exception */ @Test public void testVoidResponseTypeFireAndForgetCollapsing2() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); TestCollapserWithVoidResponseTypeAndMissingMapResponseToRequests collapser1 = new TestCollapserWithVoidResponseTypeAndMissingMapResponseToRequests(timer, 1); Future<Void> response1 = collapser1.queue(); new TestCollapserWithVoidResponseTypeAndMissingMapResponseToRequests(timer, 2).queue(); timer.incrementTime(100); // let time pass that equals the default delay/period // we will fetch one of these just so we wait for completion ... but expect an error try { assertEquals(null, response1.get(1000, TimeUnit.MILLISECONDS)); fail("expected an error as mapResponseToRequests did not set responses"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof IllegalStateException); assertTrue(e.getCause().getMessage().startsWith("No response set by")); } assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } /** * Test a Void response type with execute - response being set in mapResponseToRequest to null * * @throws Exception */ @Test public void testVoidResponseTypeFireAndForgetCollapsing3() throws Exception { CollapserTimer timer = new RealCollapserTimer(); TestCollapserWithVoidResponseType collapser1 = new TestCollapserWithVoidResponseType(timer, 1); assertNull(collapser1.execute()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(1, cmdIterator.next().getNumberCollapsed()); } @Test public void testEarlyUnsubscribeExecutedViaToObservable() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.toObservable(); HystrixCollapser<List<String>, String, String> collapser2 = new TestRequestCollapser(timer, 2); Observable<String> response2 = collapser2.toObservable(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); s1.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertEquals("2", value2.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixCollapserMetrics metrics = collapser1.getMetrics(); assertSame(metrics, collapser2.getMetrics()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertEquals(1, command.getNumberCollapsed()); //1 should have been removed from batch } @Test public void testEarlyUnsubscribeExecutedViaObserve() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new TestRequestCollapser(timer, 2); Observable<String> response2 = collapser2.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); s1.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertEquals("2", value2.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixCollapserMetrics metrics = collapser1.getMetrics(); assertSame(metrics, collapser2.getMetrics()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertEquals(1, command.getNumberCollapsed()); //1 should have been removed from batch } @Test public void testEarlyUnsubscribeFromAllCancelsBatch() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new TestRequestCollapser(timer, 2); Observable<String> response2 = collapser2.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); s1.unsubscribe(); s2.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertNull(value2.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(0, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); } @Test public void testRequestThenCacheHitAndCacheHitUnsubscribed() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); s2.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertEquals("foo", value1.get()); assertNull(value2.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertCommandExecutionEvents(command, HystrixEventType.SUCCESS, HystrixEventType.COLLAPSED); assertEquals(1, command.getNumberCollapsed()); //should only be 1 collapsed - other came from cache, then was cancelled } @Test public void testRequestThenCacheHitAndOriginalUnsubscribed() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); s1.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertEquals("foo", value2.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertCommandExecutionEvents(command, HystrixEventType.SUCCESS, HystrixEventType.COLLAPSED); assertEquals(1, command.getNumberCollapsed()); //should only be 1 collapsed - other came from cache, then was cancelled } @Test public void testRequestThenTwoCacheHitsOriginalAndOneCacheHitUnsubscribed() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); HystrixCollapser<List<String>, String, String> collapser3 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response3 = collapser3.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final CountDownLatch latch3 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); final AtomicReference<String> value3 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); Subscription s3 = response3 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s3 Unsubscribed!"); latch3.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s3 OnCompleted"); latch3.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s3 OnError : " + e); latch3.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s3 OnNext : " + s); value3.set(s); } }); s1.unsubscribe(); s3.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertEquals("foo", value2.get()); assertNull(value3.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator().next(); assertCommandExecutionEvents(command, HystrixEventType.SUCCESS, HystrixEventType.COLLAPSED); assertEquals(1, command.getNumberCollapsed()); //should only be 1 collapsed - other came from cache, then was cancelled } @Test public void testRequestThenTwoCacheHitsAllUnsubscribed() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixCollapser<List<String>, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixCollapser<List<String>, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); HystrixCollapser<List<String>, String, String> collapser3 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response3 = collapser3.observe(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); final CountDownLatch latch3 = new CountDownLatch(1); final AtomicReference<String> value1 = new AtomicReference<String>(null); final AtomicReference<String> value2 = new AtomicReference<String>(null); final AtomicReference<String> value3 = new AtomicReference<String>(null); Subscription s1 = response1 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s1 Unsubscribed!"); latch1.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s1 OnCompleted"); latch1.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s1 OnError : " + e); latch1.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s1 OnNext : " + s); value1.set(s); } }); Subscription s2 = response2 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s2 Unsubscribed!"); latch2.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s2 OnCompleted"); latch2.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s2 OnError : " + e); latch2.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s2 OnNext : " + s); value2.set(s); } }); Subscription s3 = response3 .doOnUnsubscribe(new Action0() { @Override public void call() { System.out.println(System.currentTimeMillis() + " : s3 Unsubscribed!"); latch3.countDown(); } }) .subscribe(new Subscriber<String>() { @Override public void onCompleted() { System.out.println(System.currentTimeMillis() + " : s3 OnCompleted"); latch3.countDown(); } @Override public void onError(Throwable e) { System.out.println(System.currentTimeMillis() + " : s3 OnError : " + e); latch3.countDown(); } @Override public void onNext(String s) { System.out.println(System.currentTimeMillis() + " : s3 OnNext : " + s); value3.set(s); } }); s1.unsubscribe(); s2.unsubscribe(); s3.unsubscribe(); timer.incrementTime(10); // let time pass that equals the default delay/period assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)); assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)); assertNull(value1.get()); assertNull(value2.get()); assertNull(value3.get()); System.out.println("ReqLog : " + HystrixRequestLog.getCurrentRequest().getExecutedCommandsAsString()); assertEquals(0, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); } protected void assertCommandExecutionEvents(HystrixInvokableInfo<?> command, HystrixEventType... expectedEventTypes) { boolean emitExpected = false; int expectedEmitCount = 0; boolean fallbackEmitExpected = false; int expectedFallbackEmitCount = 0; List<HystrixEventType> condensedEmitExpectedEventTypes = new ArrayList<HystrixEventType>(); for (HystrixEventType expectedEventType: expectedEventTypes) { if (expectedEventType.equals(HystrixEventType.EMIT)) { if (!emitExpected) { //first EMIT encountered, add it to condensedEmitExpectedEventTypes condensedEmitExpectedEventTypes.add(HystrixEventType.EMIT); } emitExpected = true; expectedEmitCount++; } else if (expectedEventType.equals(HystrixEventType.FALLBACK_EMIT)) { if (!fallbackEmitExpected) { //first FALLBACK_EMIT encountered, add it to condensedEmitExpectedEventTypes condensedEmitExpectedEventTypes.add(HystrixEventType.FALLBACK_EMIT); } fallbackEmitExpected = true; expectedFallbackEmitCount++; } else { condensedEmitExpectedEventTypes.add(expectedEventType); } } List<HystrixEventType> actualEventTypes = command.getExecutionEvents(); assertEquals(expectedEmitCount, command.getNumberEmissions()); assertEquals(expectedFallbackEmitCount, command.getNumberFallbackEmissions()); assertEquals(condensedEmitExpectedEventTypes, actualEventTypes); } private static class TestRequestCollapser extends HystrixCollapser<List<String>, String, String> { private final String value; private ConcurrentLinkedQueue<HystrixCommand<List<String>>> commandsExecuted; public TestRequestCollapser(TestCollapserTimer timer, int value) { this(timer, String.valueOf(value)); } public TestRequestCollapser(TestCollapserTimer timer, String value) { this(timer, value, 10000, 10); } public TestRequestCollapser(Scope scope, TestCollapserTimer timer, String value) { this(scope, timer, value, 10000, 10); } public TestRequestCollapser(TestCollapserTimer timer, String value, ConcurrentLinkedQueue<HystrixCommand<List<String>>> executionLog) { this(timer, value, 10000, 10, executionLog); } public TestRequestCollapser(TestCollapserTimer timer, int value, int defaultMaxRequestsInBatch, int defaultTimerDelayInMilliseconds) { this(timer, String.valueOf(value), defaultMaxRequestsInBatch, defaultTimerDelayInMilliseconds); } public TestRequestCollapser(TestCollapserTimer timer, String value, int defaultMaxRequestsInBatch, int defaultTimerDelayInMilliseconds) { this(timer, value, defaultMaxRequestsInBatch, defaultTimerDelayInMilliseconds, null); } public TestRequestCollapser(Scope scope, TestCollapserTimer timer, String value, int defaultMaxRequestsInBatch, int defaultTimerDelayInMilliseconds) { this(scope, timer, value, defaultMaxRequestsInBatch, defaultTimerDelayInMilliseconds, null); } public TestRequestCollapser(TestCollapserTimer timer, String value, int defaultMaxRequestsInBatch, int defaultTimerDelayInMilliseconds, ConcurrentLinkedQueue<HystrixCommand<List<String>>> executionLog) { this(Scope.REQUEST, timer, value, defaultMaxRequestsInBatch, defaultTimerDelayInMilliseconds, executionLog); } private static HystrixCollapserMetrics createMetrics() { HystrixCollapserKey key = HystrixCollapserKey.Factory.asKey("COLLAPSER_ONE"); return HystrixCollapserMetrics.getInstance(key, new HystrixPropertiesCollapserDefault(key, HystrixCollapserProperties.Setter())); } public TestRequestCollapser(Scope scope, TestCollapserTimer timer, String value, int defaultMaxRequestsInBatch, int defaultTimerDelayInMilliseconds, ConcurrentLinkedQueue<HystrixCommand<List<String>>> executionLog) { // use a CollapserKey based on the CollapserTimer object reference so it's unique for each timer as we don't want caching // of properties to occur and we're using the default HystrixProperty which typically does caching super(collapserKeyFromString(timer), scope, timer, HystrixCollapserProperties.Setter().withMaxRequestsInBatch(defaultMaxRequestsInBatch).withTimerDelayInMilliseconds(defaultTimerDelayInMilliseconds), createMetrics()); this.value = value; this.commandsExecuted = executionLog; } @Override public String getRequestArgument() { return value; } @Override public HystrixCommand<List<String>> createCommand(final Collection<CollapsedRequest<String, String>> requests) { /* return a mocked command */ HystrixCommand<List<String>> command = new TestCollapserCommand(requests); if (commandsExecuted != null) { commandsExecuted.add(command); } return command; } @Override public void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, String>> requests) { // for simplicity I'll assume it's a 1:1 mapping between lists ... in real implementations they often need to index to maps // to allow random access as the response size does not match the request size if (batchResponse.size() != requests.size()) { throw new RuntimeException("lists don't match in size => " + batchResponse.size() + " : " + requests.size()); } int i = 0; for (CollapsedRequest<String, String> request : requests) { request.setResponse(batchResponse.get(i++)); } } } /** * Shard on the artificially provided 'type' variable. */ private static class TestShardedRequestCollapser extends TestRequestCollapser { public TestShardedRequestCollapser(TestCollapserTimer timer, String value) { super(timer, value); } @Override protected Collection<Collection<CollapsedRequest<String, String>>> shardRequests(Collection<CollapsedRequest<String, String>> requests) { Collection<CollapsedRequest<String, String>> typeA = new ArrayList<CollapsedRequest<String, String>>(); Collection<CollapsedRequest<String, String>> typeB = new ArrayList<CollapsedRequest<String, String>>(); for (CollapsedRequest<String, String> request : requests) { if (request.getArgument().endsWith("a")) { typeA.add(request); } else if (request.getArgument().endsWith("b")) { typeB.add(request); } } ArrayList<Collection<CollapsedRequest<String, String>>> shards = new ArrayList<Collection<CollapsedRequest<String, String>>>(); shards.add(typeA); shards.add(typeB); return shards; } } /** * Test the global scope */ private static class TestGloballyScopedRequestCollapser extends TestRequestCollapser { public TestGloballyScopedRequestCollapser(TestCollapserTimer timer, String value) { super(Scope.GLOBAL, timer, value); } } /** * Throw an exception when creating a command. */ private static class TestRequestCollapserWithFaultyCreateCommand extends TestRequestCollapser { public TestRequestCollapserWithFaultyCreateCommand(TestCollapserTimer timer, String value) { super(timer, value); } @Override public HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, String>> requests) { throw new RuntimeException("some failure"); } } /** * Throw an exception when creating a command. */ private static class TestRequestCollapserWithShortCircuitedCommand extends TestRequestCollapser { public TestRequestCollapserWithShortCircuitedCommand(TestCollapserTimer timer, String value) { super(timer, value); } @Override public HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, String>> requests) { // args don't matter as it's short-circuited return new ShortCircuitedCommand(); } } /** * Throw an exception when mapToResponse is invoked */ private static class TestRequestCollapserWithFaultyMapToResponse extends TestRequestCollapser { public TestRequestCollapserWithFaultyMapToResponse(TestCollapserTimer timer, String value) { super(timer, value); } @Override public void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, String>> requests) { // pretend we blow up with an NPE throw new NullPointerException("batchResponse was null and we blew up"); } } private static class TestCollapserCommand extends TestHystrixCommand<List<String>> { private final Collection<CollapsedRequest<String, String>> requests; TestCollapserCommand(Collection<CollapsedRequest<String, String>> requests) { super(testPropsBuilder().setCommandPropertiesDefaults(HystrixCommandPropertiesTest.getUnitTestPropertiesSetter().withExecutionTimeoutInMilliseconds(500))); this.requests = requests; } @Override protected List<String> run() { System.out.println(">>> TestCollapserCommand run() ... batch size: " + requests.size()); // simulate a batch request ArrayList<String> response = new ArrayList<String>(); for (CollapsedRequest<String, String> request : requests) { if (request.getArgument() == null) { response.add("NULL"); } else { if (request.getArgument().equals("FAILURE")) { throw new NullPointerException("Simulated Error"); } if (request.getArgument().equals("TIMEOUT")) { try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } response.add(request.getArgument()); } } return response; } } /** * A Command implementation that supports caching. */ private static class SuccessfulCacheableCollapsedCommand extends TestRequestCollapser { private final boolean cacheEnabled; public SuccessfulCacheableCollapsedCommand(TestCollapserTimer timer, String value, boolean cacheEnabled) { super(timer, value); this.cacheEnabled = cacheEnabled; } public SuccessfulCacheableCollapsedCommand(TestCollapserTimer timer, String value, boolean cacheEnabled, ConcurrentLinkedQueue<HystrixCommand<List<String>>> executionLog) { super(timer, value, executionLog); this.cacheEnabled = cacheEnabled; } @Override public String getCacheKey() { if (cacheEnabled) return "aCacheKey_" + super.value; else return null; } } private static class ShortCircuitedCommand extends HystrixCommand<List<String>> { protected ShortCircuitedCommand() { super(HystrixCommand.Setter.withGroupKey( HystrixCommandGroupKey.Factory.asKey("shortCircuitedCommand")) .andCommandPropertiesDefaults(HystrixCommandPropertiesTest .getUnitTestPropertiesSetter() .withCircuitBreakerForceOpen(true))); } @Override protected List<String> run() throws Exception { System.out.println("*** execution (this shouldn't happen)"); // this won't ever get called as we're forcing short-circuiting ArrayList<String> values = new ArrayList<String>(); values.add("hello"); return values; } } private static class FireAndForgetCommand extends HystrixCommand<Void> { protected FireAndForgetCommand(List<Integer> values) { super(HystrixCommand.Setter.withGroupKey( HystrixCommandGroupKey.Factory.asKey("fireAndForgetCommand")) .andCommandPropertiesDefaults(HystrixCommandPropertiesTest.getUnitTestPropertiesSetter())); } @Override protected Void run() throws Exception { System.out.println("*** FireAndForgetCommand execution: " + Thread.currentThread()); return null; } } /* package */ static class TestCollapserTimer implements CollapserTimer { private final ConcurrentLinkedQueue<ATask> tasks = new ConcurrentLinkedQueue<ATask>(); @Override public Reference<TimerListener> addListener(final TimerListener collapseTask) { tasks.add(new ATask(new TestTimerListener(collapseTask))); /** * This is a hack that overrides 'clear' of a WeakReference to match the required API * but then removes the strong-reference we have inside 'tasks'. * <p> * We do this so our unit tests know if the WeakReference is cleared correctly, and if so then the ATack is removed from 'tasks' */ return new SoftReference<TimerListener>(collapseTask) { @Override public void clear() { // super.clear(); for (ATask t : tasks) { if (t.task.actualListener.equals(collapseTask)) { tasks.remove(t); } } } }; } /** * Increment time by X. Note that incrementing by multiples of delay or period time will NOT execute multiple times. * <p> * You must call incrementTime multiple times each increment being larger than 'period' on subsequent calls to cause multiple executions. * <p> * This is because executing multiple times in a tight-loop would not achieve the correct behavior, such as batching, since it will all execute "now" not after intervals of time. * * @param timeInMilliseconds amount of time to increment */ public synchronized void incrementTime(int timeInMilliseconds) { for (ATask t : tasks) { t.incrementTime(timeInMilliseconds); } } private static class ATask { final TestTimerListener task; final int delay = 10; // our relative time that we'll use volatile int time = 0; volatile int executionCount = 0; private ATask(TestTimerListener task) { this.task = task; } public synchronized void incrementTime(int timeInMilliseconds) { time += timeInMilliseconds; if (task != null) { if (executionCount == 0) { System.out.println("ExecutionCount 0 => Time: " + time + " Delay: " + delay); if (time >= delay) { // first execution, we're past the delay time executeTask(); } } else { System.out.println("ExecutionCount 1+ => Time: " + time + " Delay: " + delay); if (time >= delay) { // subsequent executions, we're past the interval time executeTask(); } } } } private synchronized void executeTask() { System.out.println("Executing task ..."); task.tick(); this.time = 0; // we reset time after each execution this.executionCount++; System.out.println("executionCount: " + executionCount); } } } private static class TestTimerListener implements TimerListener { private final TimerListener actualListener; private final AtomicInteger count = new AtomicInteger(); public TestTimerListener(TimerListener actual) { this.actualListener = actual; } @Override public void tick() { count.incrementAndGet(); actualListener.tick(); } @Override public int getIntervalTimeInMilliseconds() { return 10; } } private static HystrixCollapserKey collapserKeyFromString(final Object o) { return new HystrixCollapserKey() { @Override public String name() { return String.valueOf(o); } }; } private static class TestCollapserWithVoidResponseType extends HystrixCollapser<Void, Void, Integer> { private final Integer value; public TestCollapserWithVoidResponseType(CollapserTimer timer, int value) { super(collapserKeyFromString(timer), Scope.REQUEST, timer, HystrixCollapserProperties.Setter().withMaxRequestsInBatch(1000).withTimerDelayInMilliseconds(50)); this.value = value; } @Override public Integer getRequestArgument() { return value; } @Override protected HystrixCommand<Void> createCommand(Collection<CollapsedRequest<Void, Integer>> requests) { ArrayList<Integer> args = new ArrayList<Integer>(); for (CollapsedRequest<Void, Integer> request : requests) { args.add(request.getArgument()); } return new FireAndForgetCommand(args); } @Override protected void mapResponseToRequests(Void batchResponse, Collection<CollapsedRequest<Void, Integer>> requests) { for (CollapsedRequest<Void, Integer> r : requests) { r.setResponse(null); } } } private static class TestCollapserWithVoidResponseTypeAndMissingMapResponseToRequests extends HystrixCollapser<Void, Void, Integer> { private final Integer value; public TestCollapserWithVoidResponseTypeAndMissingMapResponseToRequests(CollapserTimer timer, int value) { super(collapserKeyFromString(timer), Scope.REQUEST, timer, HystrixCollapserProperties.Setter().withMaxRequestsInBatch(1000).withTimerDelayInMilliseconds(50)); this.value = value; } @Override public Integer getRequestArgument() { return value; } @Override protected HystrixCommand<Void> createCommand(Collection<CollapsedRequest<Void, Integer>> requests) { ArrayList<Integer> args = new ArrayList<Integer>(); for (CollapsedRequest<Void, Integer> request : requests) { args.add(request.getArgument()); } return new FireAndForgetCommand(args); } @Override protected void mapResponseToRequests(Void batchResponse, Collection<CollapsedRequest<Void, Integer>> requests) { } } }