/** * Licensed to Cloudera, Inc. under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Cloudera, Inc. 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 com.cloudera.flume.handlers.debug; import static org.junit.Assert.*; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Random; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.junit.Before; import org.junit.Test; import com.cloudera.flume.core.Event; import com.cloudera.flume.core.EventSink; import com.cloudera.flume.core.connector.DirectDriver; import com.cloudera.flume.reporter.ReportEvent; /** * Demonstrates basic throttling works within some error-tolerance. There are * two different tests we perform here: IndividualChoke test and CollectiveChoke * test, their details are given with their respective test methods below. */ public class TestChokeDecos { public static final Logger LOG = LoggerFactory.getLogger(TestChokeDecos.class); Random rand = new Random(System.currentTimeMillis()); final ChokeManager testChokeMan = new ChokeManager(); final HashMap<String, Integer> chokeMap = new HashMap<String, Integer>(); final long testTime = 5000; // in millisecs // number of drivers created for the testing final int numDrivers = 50; // here we set the limits for the minimum and maximum throttle rates in KB/sec int minTlimit = 500; int maxTlimit = 20000; // here we set the limits on size (in Bytes) of the messages passed int minMsgSize = 50; int maxMsgSize = 30000; /* * Error tolerance constants, these ratios are the max and min limits set on * the following quantity: MaxBytes allowed/Bytes Actually shipped. Note that * the error on the upperbound is much higher than the lower, this is because * it is possible that the driver threads don't get scheduled often enough to * ship bytes close to the maximum limits. */ double highErrorLimit = 5; double lowErrorLimit = .5; @Before public void setup() { testChokeMan.setPayLoadHeaderSize(0); } /** * This extends the ChokeDecorator with the added functionality of * book-keeping the number of bytes shipped through it. */ class TestChoke<S extends EventSink> extends ChokeDecorator<S> { public TestChoke(S s, String tId) { super(s, tId); } /* * We are overriding this because the method in ChokeManager calls * super.append() and we want to avoid this as the higher-level sink is not * initialized. In this method we just eliminate that call. */ @Override public void append(Event e) throws IOException { testChokeMan.spendTokens(chokeId, e.getBody().length); updateAppendStats(e); } } /** * The high level goal of this test is to see if many drivers using different * chokes can ship the data approximately at the max-limit set on them. In * more detail, in this test we create a bunch of FakeDrivers, and for each of * these drivers we assign them a unique choke. Then we run these Drivers and * check if the amount of data shipped accross each choke is approximately * what we expect. */ @Test public void runInvidualChokeTest() throws InterruptedException, IOException { // number of chokes is equal to the number of drivers int numChokes = numDrivers; LOG.info("Setting up Individual Test"); // create some chokeIDs with random limit in the range specified for (int i = 0; i < numChokes; i++) { // different chokesIds are created with their ids coming from the range // "1", "2", "3"... // with a throttlelimit in the range [minTlimit, maxTlimit] chokeMap.put(Integer.toString(i), minTlimit + rand.nextInt(maxTlimit - minTlimit)); } // update the chokemap with these chokes testChokeMan.updateChokeLimitMap(chokeMap); // now we create bunch of chokes TestChoke[] tchokeArray = new TestChoke[numChokes]; for (int i = 0; i < numChokes; i++) { // different chokes are created with their ids coming from the range "0", // "1", "2", "3"..."numChokes" tchokeArray[i] = new TestChoke<EventSink>(null, Integer.toString(i)); } // one driver for each choke DirectDriver[] directDriverArray = new DirectDriver[numDrivers]; for (int i = 0; i < numDrivers; i++) { // Driver i is mapped to ith choke, simple 1 to 1 mapping. directDriverArray[i] = new DirectDriver("TestDriver" + i, new SynthSourceRndSize(0, minMsgSize, maxMsgSize), tchokeArray[i]); } // check if all the ChokeIDs are present in the chokeMap LOG.info("Running the Individual Test Now!"); for (int i = 0; i < numDrivers; i++) { if (!testChokeMan.isChokeId(Integer.toString(i))) { LOG.error("ChokeID " + Integer.toString(i) + "not present"); fail("ChokeID " + Integer.toString(i) + "not present"); } } // Now we start the test. // Start the ChokeManager. testChokeMan.start(); for (DirectDriver d : directDriverArray) { d.start(); } // stop for the allotted time period Thread.sleep(testTime); // Stop everything! for (DirectDriver d : directDriverArray) { d.stop(); } testChokeMan.halt(); // Now do the error evaluation, see how many bits were actually shipped. double errorRatio = 1.0; for (TestChoke<EventSink> t : tchokeArray) { // Now we compute the error ratio: Max/Actual. // Where Max= Maximum bytes which should have been shipped based on the // limit on this choke, and actual= bytes that were actually shipped. errorRatio = ((double) (chokeMap.get(t.getChokeId()) * testTime)) / (double) (t.getReport().getLongMetric("number of bytes")); LOG.info("ChokeID: " + t.getChokeId() + ", error-ratio: " + errorRatio); ReportEvent r = t.getReport(); LOG.info(" events :" + r.getLongMetric("number of events")); // Test if the error ratio is in the limit we want. assertFalse((errorRatio > this.highErrorLimit || errorRatio < this.lowErrorLimit)); } LOG.info("Individual Test successful !!!"); } /** * The high level goal of this test is to make many driver threads contend * together on same chokes and see if they collectively ship the bytes under * the limits we want. We create just few chokes here, and for each Driver we * assign them one of these chokes at random. */ @Test public void runCollectiveChokeTest() throws InterruptedException, IOException { // Few Chokes int numChokes = 5; LOG.info("Setting up Collective Test"); // create chokeIDs with random limit range for (int i = 0; i < numChokes; i++) { // different chokesIds are created with their ids coming from the range // "0", "1", "2", "3"... // with a throttlelimit in the range [minTlimit, maxTlimit] chokeMap.put(Integer.toString(i), minTlimit + rand.nextInt(maxTlimit - minTlimit)); } // update the chokemap with these chokes testChokeMan.updateChokeLimitMap(chokeMap); // Initialize the chokes appropriately. TestChoke[] tchokeArray = new TestChoke[numChokes]; for (int i = 0; i < numChokes; i++) { // different chokes are created with their ids coming from the range "0", // "1", "2", "3"..."numFakeDrivers" tchokeArray[i] = new TestChoke<EventSink>(null, Integer.toString(i)); } // As we are assigning the chokes to drivers at random, there is a chance // that not all initialized chokes are assigned to some driver. So the // number of bytes shipped on these chokes will be zero, which will throw us // off in the error evaluation. For this reason we add all the chokes // assigned to some driver in a set, and do error evaluation only on those // chokes. // chokesUsed is the set of chokes assigned to some driver. Set<TestChoke<EventSink>> chokesUsed = new HashSet<TestChoke<EventSink>>(); DirectDriver[] directDriverArray = new DirectDriver[numDrivers]; // Each driver is randomly assigned to a random choke in the range // [0,numChokes) int randChokeIndex = 0; for (int i = 0; i < numDrivers; i++) { randChokeIndex = rand.nextInt(numChokes); // DirectDriverArray[i] = new DirectDriver(new SynthSourceRndSize(0, // minMsgSize, maxMsgSize), tchokeArray[randChokeIndex]); directDriverArray[i] = new DirectDriver(new SynthSourceRndSize(0, minMsgSize, maxMsgSize), tchokeArray[randChokeIndex]); // adds this choke to the set of chokesUsed chokesUsed.add(tchokeArray[randChokeIndex]); } // check if all the ChokeIDs are present LOG.info("Running the Collective Test Now!"); for (TestChoke<EventSink> t : chokesUsed) { if (!testChokeMan.isChokeId(t.getChokeId())) { LOG.error("ChokeID " + t.getChokeId() + "not present"); fail(); } } // Now we start the test. // start the ChokeManager testChokeMan.start(); for (DirectDriver f : directDriverArray) { f.start(); } // stop for the allotted time period Thread.sleep(testTime); // Stop everything! for (DirectDriver f : directDriverArray) { f.stop(); } testChokeMan.halt(); // now do the error evaluation double errorRatio = 1.0; for (TestChoke<EventSink> t : chokesUsed) { // Now we compute the error ratio: Max/Actual. // Where Max= Maximum bytes which should have been shipped based on the // limit on this choke, and actual= bytes that were actually shipped. errorRatio = ((double) (chokeMap.get(t.getChokeId()) * testTime)) / (double) (t.getReport().getLongMetric("number of bytes")); LOG.info("ChokeID: " + t.getChokeId() + ", error-ratio: " + errorRatio); // Test if the error ratio is in the limit we want. assertFalse((errorRatio > this.highErrorLimit || errorRatio < this.lowErrorLimit)); } LOG.info("Collective test successful !!!"); } }