package com.yahoo.dtf.actions.flowcontrol;
import com.yahoo.dtf.actions.Action;
import com.yahoo.dtf.actions.properties.Property;
import com.yahoo.dtf.distribution.DistWorkState;
import com.yahoo.dtf.distribution.DistWorker;
import com.yahoo.dtf.distribution.Distribution;
import com.yahoo.dtf.distribution.DistributionFactory;
import com.yahoo.dtf.exception.DTFException;
import com.yahoo.dtf.exception.InterruptionException;
import com.yahoo.dtf.exception.ParseException;
import com.yahoo.dtf.range.Range;
import com.yahoo.dtf.range.RangeFactory;
import com.yahoo.dtf.recorder.Event;
import com.yahoo.dtf.state.DTFState;
import com.yahoo.dtf.util.ThreadUtil;
import com.yahoo.dtf.util.TimeUtil;
/**
* @dtf.tag distribute
*
* @dtf.since 1.0
* @dtf.author Rodney Gomes
*
* @dtf.tag.desc The distribute tag allows you to efficiently distribute action
* execution over time. This tag will simulate execution
* distributions and record events when desired distributions were
* not achieved during the execution of underlying actions. When
* you use the id attribute you'll be able to get events back from
* this tag during its execution as to what interval of time failed
* to met the requested work goal.
* <br/><br/>
* Distribute can be used to create load patterns because the func
* attribute allows you to define precisely what type of load
* you want this tag to generate. The currently available functions
* are defined below and more can be added upon request.
* <br/><br/>
*
* @dtf.event WORKER_ID
* @dtf.event.attr workunit
* @dtf.event.attr.desc this counts the number of units of work that were done
* and also lets you know for each event which unit of
* work was being handled.
*
* @dtf.event WORKER_ID
* @dtf.event.attr workdone
* @dtf.event.attr.desc this field will contain the exact amount of work that
* was done during the unit of time identified by the
* workunit field.
*
* @dtf.event WORKER_ID
* @dtf.event.attr workgoal
* @dtf.event.attr.desc this field will contain the exact amount of work that
* was the goal during the unit of time identified by the
* workunit field.
*
* @dtf.tag.example
* <distribute id="dist1"
* property="iter"
* timer="5m"
* range="1..5"
* func="const(20)"
* unit="1m">
* <log>Executing action from thread ${dist1.worker}!</log>
* </distribute>
*
* @dtf.tag.example
* <distribute id="dist1"
* property="iter"
* timer="5m"
* range="1..32"
* func="list(10,20,50,20,10)">
* <log>Executing action from thread ${dist1.worker}!</log>
* </distribute>
*/
public class Distribute extends Action {
/**
* @dtf.attr workers
* @dtf.attr.desc number of worker threads to use to try to guarantee the
* distribution specified. This value is under the control
* of the user and should be used wisely. If you start too
* many threads you can hose the system you're running the
* distribution on, but if you don't start enough then you
* can't meet your distribution requirements.
*
* @deprecated use the range attribute instead and be aware
* that this attribute will be removed in a future
* release.
*/
@Deprecated
private String workers = null;
/**
* @dtf.attr range
* @dtf.attr.desc Similar to the way ranges are used in the {@dtf.link for}
* and {@dtf.link parallelloop} tags. Ranges are used here to
* identify the number of underlying threads used and assign
* them a unique id based on the ids in the range.
*/
private String range = null;
/**
* @dtf.attr property
* @dtf.attr.desc the property to save the current iteartion value in. This
* is useful for generate unique identifiers in each of the
* underlying executions. This property is guaranteed to be
* unique throughout the whole run even with parallelized
* access to the value. The iteration property always starts
* at 1.
*/
private String property = null;
/**
* @dtf.attr iterations
* @dtf.attr.desc the number of iterations to run for. Once reached the test
* will come to an end even if there is a value for the timer
* property.
*/
private String iterations = null;
/**
* @dtf.attr timer
* @dtf.attr.desc time based execution can be achieved with this property,
* just put a value in the format of 1s, 2h or 3d (3 days)
* and the distribute tag will keep executing the underlying
* action till at least that much time has gone by.
*/
private String timer = null;
/**
* @dtf.attr func
* @dtf.attr.desc the actual distribution function to use during the
* execution of this test. The currently available
* distribution functions are:
* <br/>
* <b>Func Types</b>
* <table border="1">
* <tr>
* <th>Type</th>
* <th>Description</th>
* <th>Example</th>
* </tr>
* <tr>
* <td>const</td>
* <td>defines a constant distribution where the
* first argument to this function is the number
* of operations to guarantee for each unit of
* time.</td>
* <td>
* <ul>
* <li>const(10) would try to execute 10
* operations per unit of time specified
* with the attribute unit.
* </li>
* </ul>
* </td>
* </tr>
* <tr>
* <td>step</td>
* <td>defines a step function for the distribution,
* where the first argument is the starting
* value, the second argument is the stepping size
* and the 3rd argument is the duration of each
* step.</td>
* <td>
* <ul>
* <li>step(1,1,10) would start by executing 1
* operation per unit of time and then up
* by 1 operation every 10 units of time.
* </li>
* <li>step(0,10,1) would start by executing 0
* operation per unit of time and then up
* by 10 operation every 1 units of time.
* </li>
* </ul>
* </td>
* </tr>
* <tr>
* <td>list</td>
* <td>defines a list of the values, where each value
* is the exact amount of operations per unit of
* time to do in each unit of time. The values of
* this list are used once per unit of time till
* we reach the end and then we just jump back to
* the start of the list.</td>
* <td>
* <ul>
* <li>list(0,20,25,20) would try to execute
* 0 operations for the unit of time 0 and
* then 20 at time 1, 25 at time 2 and then
* 20 at unit of time 3, followed by
* starting from the beginning of the list.
* </li>
* </ul>
* </td>
* </tr>
* <tr>
* <td>limit</td>
* <td>this function will limit the underlying
* distribution function to a certain number so
* that it can't go over that.</td>
* <td>
* <ul>
* <li>limit(step(0,20,1),60) would basically
* start at 0 operations per unit of time
* and then increases by 20 operations
* every unit of time until we hit 60 at
* which will just stay constant at 60
* operations per unit of time.
* </li>
* </ul>
* </td>
* </tr>
* </table>
*
*/
private String func = null;
/**
* @dtf.attr unit
* @dtf.attr.desc the units to measure the current execution in, the units
* can be specified in the same foramt you would specify the
* timer property using the syntax 1s (1 second), 2d (2 days)
* and can't be smaller than 1 second because of precision
* requirements.
*/
private String unit = null;
/**
* @dtf.attr id
* @dtf.attr.desc the id attribute is used to generate the events being
* thrown about the work that was and wasn't completed during
* the execution of this distribution. Those events are
* defined in the previous event section. The id also makes
* the property ${[your distribute id].worker} available and
* allows you to identify during each execution which worker
* (i.e. thread) was handling the request....
*/
private String id = null;
public Distribute() { }
public void execute() throws DTFException {
int workerCount = getWorkers();
Range range = null;
if ( workerCount == -1 ) {
// range should be defined
if ( getRange() == null )
throw new ParseException("range attribute must be set.");
range = RangeFactory.getRange(getRange());
workerCount = range.size();
}
Sequence children = new Sequence();
children.addActions(children());
DistWorker[] workers = new DistWorker[workerCount];
DistributionFactory df = DistributionFactory.getInstance();
Distribution dist = null;
String id = getId();
long iterations = getIterations();
String property = getProperty();
long interval = -1;
if (getTimer() != null)
interval = TimeUtil.parseTime("timer",getTimer());
long unitWork = -1;
if (getUnit() != null)
unitWork = TimeUtil.parseTime("unit",getUnit());
if (getFunc() != null)
dist = df.getDistribution(getFunc());
DistWorkState dstate = new DistWorkState();
if ( range != null ) {
int count = 0;
while ( range.hasMoreElements() ) {
String wid = range.nextElement();
DTFState state = getState().duplicate();
if (id != null)
state.getConfig().setProperty(id + ".worker", "" + wid, true);
workers[count++] = new DistWorker(children, dstate, state);
}
} else { // obviously at this point we have workerCount set
for (int i = 0 ; i < workerCount; i++) {
DTFState state = getState().duplicate();
if (id != null)
state.getConfig().setProperty(id + ".worker", "" +i, true);
workers[i] = new DistWorker(children, dstate, state);
}
}
for (int i = 0 ; i < workerCount; i++)
workers[i].start();
long counter = 0;
long currentTime = 0;
long start = System.currentTimeMillis();
while ((iterations == -1 || counter < iterations) &&
(interval == -1 || System.currentTimeMillis() - start < interval)) {
if (dist != null) {
long workDone = 0;
long elapsedTime = 0;
long workGoal = dist.result(currentTime);
long unitStart = System.currentTimeMillis();
while ( workDone < workGoal && elapsedTime < unitWork &&
(iterations == -1 || counter < iterations) &&
(interval == -1 || (System.currentTimeMillis() - start) < interval)) {
Sequence sequence = new Sequence();
if ( property != null ) {
sequence.addAction(new Property(property,
""+counter,
true));
}
dstate.wakeUp(sequence);
workDone++;
counter++;
elapsedTime = (System.currentTimeMillis() - unitStart);
}
if (id != null) {
Event event = new Event(id);
event.addAttribute("workunit", currentTime);
event.addAttribute("workdone", workDone);
event.addAttribute("workgoal", workGoal);
event.start();
event.stop();
Action.getRecorder().record(event);
}
if ( elapsedTime < unitWork )
ThreadUtil.pause(unitWork-elapsedTime);
currentTime++;
} else {
Sequence sequence = new Sequence();
if ( property != null )
sequence.addAction(new Property(property, "" + counter, true));
dstate.wakeUp(sequence);
counter++;
}
}
dstate.allDone();
/*
* Wait for all underlying threads to terminate and log all of them
* except the last exception which we can throw to any parent tag to
* handle the error.
*/
DTFException lastexception = null;
boolean interrupted = false;
for (int index = 0; index < workers.length; index++) {
try {
workers[index].waitFor();
} catch (InterruptionException e) {
if ( getLogger().isDebugEnabled() ) {
getLogger().debug("Thread interrupted [" +
workers[index].getName() + "]",e);
}
interrupted = true;
} catch (DTFException e) {
if ( lastexception != null )
getLogger().error("Child failed.", lastexception);
lastexception = e;
}
}
if ( interrupted && getLogger().isDebugEnabled() )
getLogger().debug("Execution interrupted.");
if ( lastexception != null )
throw lastexception;
}
public String getProperty() throws ParseException { return replaceProperties(property); }
public void setProperty(String property) { this.property = property; }
@Deprecated
public int getWorkers() throws ParseException { return toInt("workers", replaceProperties(workers),-1); }
/**
* @deprecated use the attribute range to replace this, like so range="0..workers"
*/
@Deprecated
public void setWorkers(String workers) { this.workers = workers; }
public String getRange() throws ParseException { return replaceProperties(range); }
public void setRange(String range) throws ParseException { this.range = range; }
public String getTimer() throws ParseException { return replaceProperties(timer); }
public void setTimer(String timer) { this.timer = timer; }
public String getUnit() throws ParseException { return replaceProperties(unit); }
public void setUnit(String unit) { this.unit = unit; }
public long getIterations() throws ParseException { return toLong("iterations", replaceProperties(iterations),-1); }
public void setIterations(String iterations) { this.iterations = iterations; }
public String getFunc() throws ParseException { return replaceProperties(func); }
public void setFunc(String func) { this.func = func; }
public String getId() throws ParseException { return replaceProperties(id); }
public void setId(String id) { this.id = id; }
}