/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.restli.throttling; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.hadoop.conf.Configuration; import com.google.common.collect.Maps; import com.google.inject.Key; import com.google.inject.name.Names; import com.typesafe.config.ConfigFactory; import gobblin.broker.BrokerConfigurationKeyGenerator; import gobblin.broker.iface.SharedResourcesBroker; import gobblin.util.limiter.Limiter; import gobblin.util.limiter.MockRequester; import gobblin.util.limiter.RestliServiceBasedLimiter; import gobblin.util.limiter.broker.SharedLimiterKey; import gobblin.util.limiter.stressTest.RateComputingLimiterContainer; import gobblin.util.limiter.stressTest.StressTestUtils; import gobblin.util.limiter.stressTest.Stressor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * A stress test for throttling service. It creates a number of threads, each one running a stressor using a mock * {@link RestliServiceBasedLimiter}. * * The mock {@link RestliServiceBasedLimiter} sends requests to an embedded {@link LimiterServerResource}, adding an * artificial latency to the requests representing the network latency. * * The stress test prints permit granting statistics every 15 seconds. */ @Slf4j public class LocalStressTest { public static final Option STRESSOR_THREADS = new Option("stressorThreads", true, "Number of stressor threads"); public static final Option PROCESSOR_THREADS = new Option("processorThreads", true, "Number of request processor threads."); public static final Option ARTIFICIAL_LATENCY = new Option("latency", true, "Artificial request latency in millis."); public static final Option QPS = new Option("qps", true, "Target qps."); public static final Options OPTIONS = StressTestUtils.OPTIONS.addOption(STRESSOR_THREADS).addOption(PROCESSOR_THREADS); public static final int DEFAULT_STRESSOR_THREADS = 10; public static final int DEFAULT_PROCESSOR_THREADS = 10; public static final int DEFAULT_ARTIFICIAL_LATENCY = 100; public static final int DEFAULT_TARGET_QPS = 100; public static void main(String[] args) throws Exception { CommandLine cli = StressTestUtils.parseCommandLine(OPTIONS, args); int stressorThreads = Integer.parseInt(cli.getOptionValue(STRESSOR_THREADS.getOpt(), Integer.toString( DEFAULT_STRESSOR_THREADS))); int processorThreads = Integer.parseInt(cli.getOptionValue(PROCESSOR_THREADS.getOpt(), Integer.toString( DEFAULT_PROCESSOR_THREADS))); int artificialLatency = Integer.parseInt(cli.getOptionValue(ARTIFICIAL_LATENCY.getOpt(), Integer.toString( DEFAULT_ARTIFICIAL_LATENCY))); long targetQps = Integer.parseInt(cli.getOptionValue(QPS.getOpt(), Integer.toString( DEFAULT_TARGET_QPS))); Configuration configuration = new Configuration(); StressTestUtils.populateConfigFromCli(configuration, cli); String resourceLimited = LocalStressTest.class.getSimpleName(); Map<String, String> configMap = Maps.newHashMap(); ThrottlingPolicyFactory factory = new ThrottlingPolicyFactory(); SharedLimiterKey res1key = new SharedLimiterKey(resourceLimited); configMap.put(BrokerConfigurationKeyGenerator.generateKey(factory, res1key, null, ThrottlingPolicyFactory.POLICY_KEY), QPSPolicy.FACTORY_ALIAS); configMap.put(BrokerConfigurationKeyGenerator.generateKey(factory, res1key, null, QPSPolicy.QPS), Long.toString(targetQps)); ThrottlingGuiceServletConfig guiceServletConfig = new ThrottlingGuiceServletConfig(); guiceServletConfig.initialize(ConfigFactory.parseMap(configMap)); LimiterServerResource limiterServer = guiceServletConfig.getInjector().getInstance(LimiterServerResource.class); RateComputingLimiterContainer limiterContainer = new RateComputingLimiterContainer(); Class<? extends Stressor> stressorClass = configuration.getClass(StressTestUtils.STRESSOR_CLASS, StressTestUtils.DEFAULT_STRESSOR_CLASS, Stressor.class); ExecutorService executorService = Executors.newFixedThreadPool(stressorThreads); SharedResourcesBroker broker = guiceServletConfig.getInjector().getInstance(Key.get(SharedResourcesBroker.class, Names.named(LimiterServerResource.BROKER_INJECT_NAME))); ThrottlingPolicy policy = (ThrottlingPolicy) broker.getSharedResource(new ThrottlingPolicyFactory(), new SharedLimiterKey(resourceLimited)); ScheduledExecutorService reportingThread = Executors.newSingleThreadScheduledExecutor(); reportingThread.scheduleAtFixedRate(new Reporter(limiterContainer, policy), 0, 15, TimeUnit.SECONDS); Queue<Future<?>> futures = new LinkedList<>(); MockRequester requester = new MockRequester(limiterServer, artificialLatency, processorThreads); requester.start(); for (int i = 0; i < stressorThreads; i++) { RestliServiceBasedLimiter restliLimiter = RestliServiceBasedLimiter.builder().resourceLimited(resourceLimited) .requestSender(requester) .serviceIdentifier("stressor" + i).build(); Stressor stressor = stressorClass.newInstance(); stressor.configure(configuration); futures.add(executorService.submit(new StressorRunner(limiterContainer.decorateLimiter(restliLimiter), stressor))); } int stressorFailures = 0; for (Future<?> future : futures) { try { future.get(); } catch (ExecutionException ee) { stressorFailures++; } } requester.stop(); executorService.shutdownNow(); if (stressorFailures > 0) { log.error("There were " + stressorFailures + " failed stressor threads."); } System.exit(stressorFailures); } @RequiredArgsConstructor private static class StressorRunner implements Runnable { private final Limiter limiter; private final Stressor stressor; @Override public void run() { try { this.limiter.start(); this.stressor.run(this.limiter); this.limiter.stop(); } catch (InterruptedException ie) { log.error("Error: ", ie); } } } @RequiredArgsConstructor private static class Reporter implements Runnable { private final RateComputingLimiterContainer limiter; private final ThrottlingPolicy policy; @Override public void run() { DescriptiveStatistics stats = limiter.getRateStatsSinceLastReport(); if (stats != null) { log.info(String.format("Requests rate stats: count: %d, min: %f, max: %f, mean: %f, std: %f, sum: %f", stats.getN(), stats.getMin(), stats.getMax(), stats.getMean(), stats.getStandardDeviation(), stats.getSum())); } stats = limiter.getUnusedPermitsSinceLastReport(); if (stats != null) { log.info(String.format("Unused permits rate stats: count: %d, min: %f, max: %f, mean: %f, std: %f, sum: %f", stats.getN(), stats.getMin(), stats.getMax(), stats.getMean(), stats.getStandardDeviation(), stats.getSum())); } if (this.policy instanceof QPSPolicy) { QPSPolicy qpsPolicy = (QPSPolicy) this.policy; DynamicTokenBucket dynamicTokenBucket = qpsPolicy.getTokenBucket(); TokenBucket tokenBucket = dynamicTokenBucket.getTokenBucket(); log.info("Stored tokens: " + tokenBucket.getStoredTokens()); } } } }