/**
* 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.perf;
import com.netflix.hystrix.HystrixCollapser;
import com.netflix.hystrix.HystrixCollapserKey;
import com.netflix.hystrix.HystrixCollapserMetrics;
import com.netflix.hystrix.HystrixCollapserProperties;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixObservableCollapser;
import com.netflix.hystrix.HystrixObservableCommand;
import com.netflix.hystrix.HystrixThreadPool;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesCollapserDefault;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.infra.Blackhole;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class ObservableCollapserPerfTest {
@State(Scope.Thread)
public static class CollapserState {
@Param({"1", "10", "100", "1000"})
int numToCollapse;
@Param({"1", "10", "100"})
int numResponsesPerArg;
@Param({"1", "1000", "1000000"})
int blackholeConsumption;
HystrixRequestContext reqContext;
Observable<String> executionHandle;
@Setup(Level.Invocation)
public void setUp() {
reqContext = HystrixRequestContext.initializeContext();
List<Observable<String>> os = new ArrayList<Observable<String>>();
for (int i = 0; i < numToCollapse; i++) {
TestCollapserWithMultipleResponses collapser = new TestCollapserWithMultipleResponses(i, numResponsesPerArg, blackholeConsumption);
os.add(collapser.observe());
}
executionHandle = Observable.merge(os);
}
@TearDown(Level.Invocation)
public void tearDown() {
reqContext.shutdown();
}
}
//TODO wire in synthetic timer
private static class TestCollapserWithMultipleResponses extends HystrixObservableCollapser<String, String, String, String> {
private final String arg;
private final static Map<String, Integer> emitsPerArg;
private final int blackholeConsumption;
private final boolean commandConstructionFails;
private final Func1<String, String> keyMapper;
private final Action1<HystrixCollapser.CollapsedRequest<String, String>> onMissingResponseHandler;
static {
emitsPerArg = new HashMap<String, Integer>();
}
public TestCollapserWithMultipleResponses(int arg, int numResponsePerArg, int blackholeConsumption) {
super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("COLLAPSER")).andScope(Scope.REQUEST).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withMaxRequestsInBatch(1000).withTimerDelayInMilliseconds(1)));
this.arg = arg + "";
emitsPerArg.put(this.arg, numResponsePerArg);
this.blackholeConsumption = blackholeConsumption;
commandConstructionFails = false;
keyMapper = new Func1<String, String>() {
@Override
public String call(String s) {
return s.substring(0, s.indexOf(":"));
}
};
onMissingResponseHandler = new Action1<HystrixCollapser.CollapsedRequest<String, String>>() {
@Override
public void call(HystrixCollapser.CollapsedRequest<String, String> collapsedReq) {
collapsedReq.setResponse("missing:missing");
}
};
}
@Override
public String getRequestArgument() {
return arg;
}
@Override
protected HystrixObservableCommand<String> createCommand(Collection<HystrixCollapser.CollapsedRequest<String, String>> collapsedRequests) {
if (commandConstructionFails) {
throw new RuntimeException("Exception thrown in command construction");
} else {
List<Integer> args = new ArrayList<Integer>();
for (HystrixCollapser.CollapsedRequest<String, String> collapsedRequest : collapsedRequests) {
String stringArg = collapsedRequest.getArgument();
int intArg = Integer.parseInt(stringArg);
args.add(intArg);
}
return new TestCollapserCommandWithMultipleResponsePerArgument(args, emitsPerArg, blackholeConsumption);
}
}
//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(HystrixCollapser.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 HystrixObservableCommand<String> {
private final List<Integer> args;
private final Map<String, Integer> emitsPerArg;
private final int blackholeConsumption;
TestCollapserCommandWithMultipleResponsePerArgument(List<Integer> args, Map<String, Integer> emitsPerArg, int blackholeConsumption) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("COLLAPSER_MULTI")).andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(500)));
this.args = args;
this.emitsPerArg = emitsPerArg;
this.blackholeConsumption = blackholeConsumption;
}
@Override
protected Observable<String> construct() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try {
Blackhole.consumeCPU(blackholeConsumption);
for (Integer arg: args) {
int numEmits = emitsPerArg.get(arg.toString());
for (int j = 1; j < numEmits + 1; j++) {
subscriber.onNext(arg + ":" + (arg * j));
}
}
} catch (Throwable ex) {
subscriber.onError(ex);
}
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.computation());
}
}
@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
public List<String> observeCollapsedAndWait(CollapserState collapserState) {
return collapserState.executionHandle.toList().toBlocking().single();
}
}