/** * Copyright 2014 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.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import com.netflix.hystrix.collapser.CollapserTimer; import com.netflix.hystrix.collapser.RealCollapserTimer; import com.netflix.hystrix.strategy.concurrency.HystrixContextRunnable; import com.netflix.hystrix.strategy.concurrency.HystrixContextScheduler; import com.netflix.hystrix.strategy.properties.HystrixPropertiesCollapserDefault; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import com.netflix.hystrix.HystrixCollapser.CollapsedRequest; import com.netflix.hystrix.HystrixCollapserTest.TestCollapserTimer; import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; import static org.junit.Assert.*; public class HystrixObservableCollapserTest { private static Action1<CollapsedRequest<String, String>> onMissingError = new Action1<CollapsedRequest<String, String>>() { @Override public void call(CollapsedRequest<String, String> collapsedReq) { collapsedReq.setException(new IllegalStateException("must have a value")); } }; private static Action1<CollapsedRequest<String, String>> onMissingThrow = new Action1<CollapsedRequest<String, String>>() { @Override public void call(CollapsedRequest<String, String> collapsedReq) { throw new RuntimeException("synchronous error in onMissingResponse handler"); } }; private static Action1<CollapsedRequest<String, String>> onMissingComplete = new Action1<CollapsedRequest<String, String>>() { @Override public void call(CollapsedRequest<String, String> collapsedReq) { collapsedReq.setComplete(); } }; private static Action1<CollapsedRequest<String, String>> onMissingIgnore = new Action1<CollapsedRequest<String, String>>() { @Override public void call(CollapsedRequest<String, String> collapsedReq) { //do nothing } }; private static Action1<CollapsedRequest<String, String>> onMissingFillIn = new Action1<CollapsedRequest<String, String>>() { @Override public void call(CollapsedRequest<String, String> collapsedReq) { collapsedReq.setResponse("fillin"); } }; private static Func1<String, String> prefixMapper = new Func1<String, String>() { @Override public String call(String s) { return s.substring(0, s.indexOf(":")); } }; private static Func1<String, String> map1To3And2To2 = new Func1<String, String>() { @Override public String call(String s) { String prefix = s.substring(0, s.indexOf(":")); if (prefix.equals("2")) { return "2"; } else { return "3"; } } }; private static Func1<String, String> mapWithErrorOn1 = new Func1<String, String>() { @Override public String call(String s) { String prefix = s.substring(0, s.indexOf(":")); if (prefix.equals("1")) { throw new RuntimeException("poorly implemented demultiplexer"); } else { return "2"; } } }; @Rule public HystrixRequestContextRule ctx = new HystrixRequestContextRule(); private static ExecutorService threadPool = new ThreadPoolExecutor(100, 100, 10, TimeUnit.MINUTES, new SynchronousQueue<Runnable>()); @Before public void init() { // since we're going to modify properties of the same class between tests, wipe the cache each time HystrixCollapser.reset(); } @Test public void testTwoRequests() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestRequestCollapser(timer, 1); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestRequestCollapser(timer, 2); Future<String> response1 = collapser1.observe().toBlocking().toFuture(); Future<String> response2 = collapser2.observe().toBlocking().toFuture(); timer.incrementTime(10); // let time pass that equals the default delay/period assertEquals("1", response1.get()); assertEquals("2", response2.get()); assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size()); Iterator<HystrixInvokableInfo<?>> cmdIterator = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().iterator(); assertEquals(2, cmdIterator.next().getNumberCollapsed()); } @Test public void stressTestRequestCollapser() throws Exception { for(int i = 0; i < 10; i++) { init(); testTwoRequests(); ctx.reset(); } } @Test public void testTwoRequestsWhichShouldEachEmitTwice() throws Exception { //TestCollapserTimer timer = new TestCollapserTimer(); CollapserTimer timer = new RealCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 3, false, false, prefixMapper, onMissingComplete); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 3, false, false, prefixMapper, onMissingComplete); TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); System.out.println(System.currentTimeMillis() + "Starting to observe collapser1"); collapser1.observe().subscribe(testSubscriber1); collapser2.observe().subscribe(testSubscriber2); System.out.println(System.currentTimeMillis() + "Done with collapser observe()s"); //Note that removing these awaits breaks the unit test. That implies that the above subscribe does not wait for a terminal event testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertValues("1:1", "1:2", "1:3"); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6"); } @Test public void testTwoRequestsWithErrorProducingBatchCommand() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 3, true); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 3, true); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertError(RuntimeException.class); testSubscriber2.assertNoValues(); } @Test public void testTwoRequestsWithErrorInDemultiplex() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 3, false, false, mapWithErrorOn1, onMissingError); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 3, false, false, mapWithErrorOn1, onMissingError); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6"); } @Test public void testTwoRequestsWithEmptyResponseAndOnMissingError() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingError); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 0, onMissingError); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(IllegalStateException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertError(IllegalStateException.class); testSubscriber2.assertNoValues(); } @Test public void testTwoRequestsWithEmptyResponseAndOnMissingThrow() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingThrow); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 0, onMissingThrow); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertError(RuntimeException.class); testSubscriber2.assertNoValues(); } @Test public void testTwoRequestsWithEmptyResponseAndOnMissingComplete() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingComplete); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 0, onMissingComplete); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertNoValues(); } @Test public void testTwoRequestsWithEmptyResponseAndOnMissingIgnore() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingIgnore); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 0, onMissingIgnore); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertNoValues(); } @Test public void testTwoRequestsWithEmptyResponseAndOnMissingFillInStaticValue() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingFillIn); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 0, onMissingFillIn); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertValues("fillin"); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("fillin"); } @Test public void testTwoRequestsWithValuesForOneArgOnlyAndOnMissingComplete() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingComplete); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 5, onMissingComplete); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6", "2:8", "2:10"); } @Test public void testTwoRequestsWithValuesForOneArgOnlyAndOnMissingFillInStaticValue() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingFillIn); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 5, onMissingFillIn); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertValues("fillin"); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6", "2:8", "2:10"); } @Test public void testTwoRequestsWithValuesForOneArgOnlyAndOnMissingIgnore() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingIgnore); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 5, onMissingIgnore); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertCompleted(); testSubscriber1.assertNoErrors(); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6", "2:8", "2:10"); } @Test public void testTwoRequestsWithValuesForOneArgOnlyAndOnMissingError() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingError); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 5, onMissingError); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(IllegalStateException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6", "2:8", "2:10"); } @Test public void testTwoRequestsWithValuesForOneArgOnlyAndOnMissingThrow() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 0, onMissingThrow); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 5, onMissingThrow); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6", "2:8", "2:10"); } @Test public void testTwoRequestsWithValuesForWrongArgs() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 3, false, false, map1To3And2To2, onMissingError); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 3, false, false, map1To3And2To2, onMissingError); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.assertNoValues(); testSubscriber2.assertCompleted(); testSubscriber2.assertNoErrors(); testSubscriber2.assertValues("2:2", "2:4", "2:6"); } @Test public void testTwoRequestsWhenBatchCommandFails() { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestCollapserWithMultipleResponses(timer, 1, 3, false, true, map1To3And2To2, onMissingError); HystrixObservableCollapser<String, String, String, String> collapser2 = new TestCollapserWithMultipleResponses(timer, 2, 3, false, true, map1To3And2To2, onMissingError); System.out.println("Starting to observe collapser1"); Observable<String> result1 = collapser1.observe(); Observable<String> result2 = collapser2.observe(); timer.incrementTime(10); // let time pass that equals the default delay/period TestSubscriber<String> testSubscriber1 = new TestSubscriber<String>(); result1.subscribe(testSubscriber1); TestSubscriber<String> testSubscriber2 = new TestSubscriber<String>(); result2.subscribe(testSubscriber2); testSubscriber1.awaitTerminalEvent(); testSubscriber2.awaitTerminalEvent(); testSubscriber1.assertError(RuntimeException.class); testSubscriber1.getOnErrorEvents().get(0).printStackTrace(); testSubscriber1.assertNoValues(); testSubscriber2.assertError(RuntimeException.class); testSubscriber2.assertNoValues(); } @Test public void testCollapserUnderConcurrency() throws InterruptedException { final CollapserTimer timer = new RealCollapserTimer(); final int NUM_THREADS_SUBMITTING_WORK = 8; final int NUM_REQUESTS_PER_THREAD = 8; final CountDownLatch latch = new CountDownLatch(NUM_THREADS_SUBMITTING_WORK); List<Runnable> runnables = new ArrayList<Runnable>(); final ConcurrentLinkedQueue<TestSubscriber<String>> subscribers = new ConcurrentLinkedQueue<TestSubscriber<String>>(); HystrixRequestContext context = HystrixRequestContext.initializeContext(); final AtomicInteger uniqueInt = new AtomicInteger(0); for (int i = 0; i < NUM_THREADS_SUBMITTING_WORK; i++) { runnables.add(new Runnable() { @Override public void run() { try { //System.out.println("Runnable starting on thread : " + Thread.currentThread().getName()); for (int j = 0; j < NUM_REQUESTS_PER_THREAD; j++) { HystrixObservableCollapser<String, String, String, String> collapser = new TestCollapserWithMultipleResponses(timer, uniqueInt.getAndIncrement(), 3, false); Observable<String> o = collapser.toObservable(); TestSubscriber<String> subscriber = new TestSubscriber<String>(); o.subscribe(subscriber); subscribers.offer(subscriber); } //System.out.println("Runnable done on thread : " + Thread.currentThread().getName()); } finally { latch.countDown(); } } }); } for (Runnable r: runnables) { threadPool.submit(new HystrixContextRunnable(r)); } assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); for (TestSubscriber<String> subscriber: subscribers) { subscriber.awaitTerminalEvent(); if (subscriber.getOnErrorEvents().size() > 0) { System.out.println("ERROR : " + subscriber.getOnErrorEvents()); for (Throwable ex: subscriber.getOnErrorEvents()) { ex.printStackTrace(); } } subscriber.assertCompleted(); subscriber.assertNoErrors(); System.out.println("Received : " + subscriber.getOnNextEvents()); subscriber.assertValueCount(3); } context.shutdown(); } @Test public void testConcurrencyInLoop() throws InterruptedException { for (int i = 0; i < 10; i++) { System.out.println("TRIAL : " + i); testCollapserUnderConcurrency(); } } @Test public void testEarlyUnsubscribeExecutedViaToObservable() throws Exception { TestCollapserTimer timer = new TestCollapserTimer(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.toObservable(); HystrixObservableCollapser<String, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new TestRequestCollapser(timer, 1); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, 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.EMIT, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, 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.EMIT, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, String, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); HystrixObservableCollapser<String, 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.EMIT, 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(); HystrixObservableCollapser<String, String, String, String> collapser1 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response1 = collapser1.observe(); HystrixObservableCollapser<String, String, String, String> collapser2 = new SuccessfulCacheableCollapsedCommand(timer, "foo", true); Observable<String> response2 = collapser2.observe(); HystrixObservableCollapser<String, 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()); } class Pair<A, B> { final A a; final B b; Pair(A a, B b) { this.a = a; this.b = b; } } class MyCommand extends HystrixObservableCommand<Pair<String, Integer>> { private final List<String> args; public MyCommand(List<String> args) { super(HystrixObservableCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BATCH"))); this.args = args; } @Override protected Observable<Pair<String, Integer>> construct() { return Observable.from(args).map(new Func1<String, Pair<String, Integer>>() { @Override public Pair<String, Integer> call(String s) { return new Pair<String, Integer>(s, Integer.parseInt(s)); } }); } } class MyCollapser extends HystrixObservableCollapser<String, Pair<String, Integer>, Integer, String> { private final String arg; public MyCollapser(String arg, boolean requestCachingOn) { super(HystrixCollapserKey.Factory.asKey("UNITTEST"), HystrixObservableCollapser.Scope.REQUEST, new RealCollapserTimer(), HystrixCollapserProperties.Setter().withRequestCacheEnabled(requestCachingOn), 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 HystrixObservableCommand<Pair<String, Integer>> createCommand(Collection<CollapsedRequest<Integer, String>> collapsedRequests) { List<String> args = new ArrayList<String>(); for (CollapsedRequest<Integer, String> req: collapsedRequests) { args.add(req.getArgument()); } return new MyCommand(args); } @Override protected Func1<Pair<String, Integer>, String> getBatchReturnTypeKeySelector() { return new Func1<Pair<String, Integer>, String>() { @Override public String call(Pair<String, Integer> pair) { return pair.a; } }; } @Override protected Func1<String, String> getRequestArgumentKeySelector() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } @Override protected void onMissingResponse(CollapsedRequest<Integer, String> r) { r.setException(new RuntimeException("missing")); } @Override protected Func1<Pair<String, Integer>, Integer> getBatchReturnTypeToResponseTypeMapper() { return new Func1<Pair<String, Integer>, Integer>() { @Override public Integer call(Pair<String, Integer> pair) { return pair.b; } }; } } @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.assertCompleted(); 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()); } 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 HystrixObservableCollapser<String, String, String, String> { private final String value; private ConcurrentLinkedQueue<HystrixObservableCommand<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(TestCollapserTimer timer, String value, ConcurrentLinkedQueue<HystrixObservableCommand<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<HystrixObservableCommand<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<HystrixObservableCommand<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 HystrixObservableCommand<String> createCommand(final Collection<CollapsedRequest<String, String>> requests) { /* return a mocked command */ HystrixObservableCommand<String> command = new TestCollapserCommand(requests); if (commandsExecuted != null) { commandsExecuted.add(command); } return command; } @Override protected Func1<String, String> getBatchReturnTypeToResponseTypeMapper() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } @Override protected Func1<String, String> getBatchReturnTypeKeySelector() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } @Override protected Func1<String, String> getRequestArgumentKeySelector() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } @Override protected void onMissingResponse(CollapsedRequest<String, String> r) { r.setException(new RuntimeException("missing value!")); } } private static HystrixCollapserKey collapserKeyFromString(final Object o) { return new HystrixCollapserKey() { @Override public String name() { return String.valueOf(o); } }; } private static class TestCollapserCommand extends TestHystrixObservableCommand<String> { private final Collection<CollapsedRequest<String, String>> requests; TestCollapserCommand(Collection<CollapsedRequest<String, String>> requests) { super(testPropsBuilder().setCommandPropertiesDefaults(HystrixCommandPropertiesTest.getUnitTestPropertiesSetter().withExecutionTimeoutInMilliseconds(1000))); this.requests = requests; } @Override protected Observable<String> construct() { return Observable.create(new OnSubscribe<String>() { @Override public void call(Subscriber<? super String> s) { System.out.println(">>> TestCollapserCommand run() ... batch size: " + requests.size()); // simulate a batch request for (CollapsedRequest<String, String> request : requests) { if (request.getArgument() == null) { throw new NullPointerException("Simulated Error"); } if (request.getArgument().equals("TIMEOUT")) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } s.onNext(request.getArgument()); } s.onCompleted(); } }).subscribeOn(Schedulers.computation()); } } private static class TestCollapserWithMultipleResponses extends HystrixObservableCollapser<String, String, String, String> { private final String arg; private final static ConcurrentMap<String, Integer> emitsPerArg; private final boolean commandConstructionFails; private final boolean commandExecutionFails; private final Func1<String, String> keyMapper; private final Action1<CollapsedRequest<String, String>> onMissingResponseHandler; private final static HystrixCollapserKey key = HystrixCollapserKey.Factory.asKey("COLLAPSER_MULTI"); private final static HystrixCollapserProperties.Setter propsSetter = HystrixCollapserProperties.Setter().withMaxRequestsInBatch(10).withTimerDelayInMilliseconds(10); private final static HystrixCollapserMetrics metrics = HystrixCollapserMetrics.getInstance(key, new HystrixPropertiesCollapserDefault(key, HystrixCollapserProperties.Setter())); static { emitsPerArg = new ConcurrentHashMap<String, Integer>(); } public TestCollapserWithMultipleResponses(CollapserTimer timer, int arg, int numEmits, boolean commandConstructionFails) { this(timer, arg, numEmits, commandConstructionFails, false, prefixMapper, onMissingComplete); } public TestCollapserWithMultipleResponses(CollapserTimer timer, int arg, int numEmits, Action1<CollapsedRequest<String, String>> onMissingHandler) { this(timer, arg, numEmits, false, false, prefixMapper, onMissingHandler); } public TestCollapserWithMultipleResponses(CollapserTimer timer, int arg, int numEmits, Func1<String, String> keyMapper) { this(timer, arg, numEmits, false, false, keyMapper, onMissingComplete); } public TestCollapserWithMultipleResponses(CollapserTimer timer, int arg, int numEmits, boolean commandConstructionFails, boolean commandExecutionFails, Func1<String, String> keyMapper, Action1<CollapsedRequest<String, String>> onMissingResponseHandler) { super(collapserKeyFromString(timer), Scope.REQUEST, timer, propsSetter, metrics); this.arg = arg + ""; emitsPerArg.put(this.arg, numEmits); this.commandConstructionFails = commandConstructionFails; this.commandExecutionFails = commandExecutionFails; this.keyMapper = keyMapper; this.onMissingResponseHandler = onMissingResponseHandler; } @Override public String getRequestArgument() { return arg; } @Override protected HystrixObservableCommand<String> createCommand(Collection<CollapsedRequest<String, String>> collapsedRequests) { assertNotNull("command creation should have HystrixRequestContext", HystrixRequestContext.getContextForCurrentThread()); if (commandConstructionFails) { throw new RuntimeException("Exception thrown in command construction"); } else { List<Integer> args = new ArrayList<Integer>(); for (CollapsedRequest<String, String> collapsedRequest : collapsedRequests) { String stringArg = collapsedRequest.getArgument(); int intArg = Integer.parseInt(stringArg); args.add(intArg); } return new TestCollapserCommandWithMultipleResponsePerArgument(args, emitsPerArg, commandExecutionFails); } } //Data comes back in the form: 1:1, 1:2, 1:3, 2:2, 2:4, 2:6. //This method should use the first half of that string as the request arg @Override protected Func1<String, String> getBatchReturnTypeKeySelector() { return keyMapper; } @Override protected Func1<String, String> getRequestArgumentKeySelector() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } @Override protected void onMissingResponse(CollapsedRequest<String, String> r) { onMissingResponseHandler.call(r); } @Override protected Func1<String, String> getBatchReturnTypeToResponseTypeMapper() { return new Func1<String, String>() { @Override public String call(String s) { return s; } }; } } private static class TestCollapserCommandWithMultipleResponsePerArgument extends TestHystrixObservableCommand<String> { private final List<Integer> args; private final Map<String, Integer> emitsPerArg; private final boolean commandExecutionFails; private static InspectableBuilder.TestCommandBuilder setter = testPropsBuilder(); TestCollapserCommandWithMultipleResponsePerArgument(List<Integer> args, Map<String, Integer> emitsPerArg, boolean commandExecutionFails) { super(setter); this.args = args; this.emitsPerArg = emitsPerArg; this.commandExecutionFails = commandExecutionFails; } @Override protected Observable<String> construct() { assertNotNull("Wiring the Batch command into the Observable chain should have a HystrixRequestContext", HystrixRequestContext.getContextForCurrentThread()); if (commandExecutionFails) { return Observable.error(new RuntimeException("Synthetic error while running batch command")); } else { return Observable.create(new OnSubscribe<String>() { @Override public void call(Subscriber<? super String> subscriber) { try { assertNotNull("Executing the Batch command should have a HystrixRequestContext", HystrixRequestContext.getContextForCurrentThread()); Thread.sleep(1); for (Integer arg : args) { int numEmits = emitsPerArg.get(arg.toString()); for (int j = 1; j < numEmits + 1; j++) { subscriber.onNext(arg + ":" + (arg * j)); Thread.sleep(1); } Thread.sleep(1); } subscriber.onCompleted(); } catch (Throwable ex) { ex.printStackTrace(); subscriber.onError(ex); } } }); } } } /** * 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; } @Override public String getCacheKey() { if (cacheEnabled) return "aCacheKey_" + super.value; else return null; } } }