001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.commons.performance;
019    
020    import java.io.File;
021    import java.util.Iterator;
022    import java.util.concurrent.ExecutorService;
023    import java.util.concurrent.Executors;
024    import java.util.concurrent.TimeUnit;
025    import java.util.logging.Logger;
026    import org.apache.commons.digester.Digester;
027     
028    /**
029     * <p>Base class for load / peformance test runners.
030     * Uses Commons Digester to parse and load configuration and spawns
031     * {@link ClientThread} instances to generate load and gather statistics.</p>
032     * 
033     * <p>Subclasses <code>must</code> implement <code>makeClientThread</code> to
034     * create client thread instances to be kicked off by <code>execute.</code>
035     * Subclasses will also in general override <code>configure</code> to load
036     * additional configuration parameters and pass them on to the client in 
037     * <code>makeClientThread.</code>  Implementations of <code>configure</code>
038     * should start with a <code>super()</code> call so that the base configuration
039     * parameters are loaded. This method should also set the <code>configFile</code>
040     * property to a valid URI or filespec (suitable argument for Digester's parse
041     * method). Setup code that needs to be executed before any client threads are
042     * spawned should be put in <code>init</code></p>
043     * 
044     * <p>See 
045     * <a href="http://svn.apache.org/viewvc/commons/sandbox/performance/trunk/src/java/org/apache/commons/performance/dbcp/DBCPSoak.java?view=markup">
046     * DBCPSoak</a> and its 
047     * <a href="http://svn.apache.org/viewvc/commons/sandbox/performance/trunk/config-dbcp.xml?view=markup">
048     * sample configuration file</a> for an example.  As in that example, additional
049     * sections of the config file should be parsed and loaded in the overridden
050     * <code>configure</code> method. The "run" section is required by the base
051     * implementation. That example also illustrates how <code>init</code>
052     * can be used to set up resources or data structures required by the client
053     * threads.</p>
054     *
055     */
056    public abstract class LoadGenerator {
057        
058        /** logger */
059        protected static final Logger logger = 
060            Logger.getLogger(LoadGenerator.class.getName());
061        
062        /** Statistics aggregator */
063        private static Statistics stats = new Statistics();
064        
065        // Client thread properties
066        protected long minDelay;
067        protected long maxDelay;
068        protected double sigma;
069        protected String delayType;
070        protected String rampType;
071        protected long rampPeriod;
072        protected long peakPeriod;
073        protected long troughPeriod;
074        protected String cycleType;
075        
076        // Run properties
077        private long numClients;
078        private long iterations;
079        
080        protected Digester digester = new Digester();
081        protected String configFile = null;
082        
083        /**
084         * <p>Invokes {@link #configure()} to load digester rules, then digster.parse,
085         * then {@link #init} to initialize configuration members. Then spawns and
086         * executes {@link #numClients} ClientThreads using {@link #makeClientThread}
087         * to create the ClientThread instances. Waits for all spawned threads to
088         * terminate and then logs accumulated statistics, using 
089         * {@link Statistics#displayOverallSummary}</p>
090         * 
091         * <p>Subclasses should not need to override this method, but must implement
092         * {@link #makeClientThread} and may override {@link #configure} and
093         * {@link #init} to prepare data to pass to <code>makeClientThread</code>, 
094         * and {@link #cleanUp} to clean up after all threads terminate.
095         * </p>
096         * 
097         * @throws Exception
098         */
099        public void execute() throws Exception {
100            configure();
101            parseConfigFile();
102            init();
103            // Spawn and execute client threads
104                    ExecutorService ex = Executors.newFixedThreadPool((int)numClients);
105                    for (int i = 0; i < numClients; i++) {
106                ClientThread clientThread = makeClientThread(iterations, minDelay,
107                        maxDelay, sigma, delayType, rampPeriod, peakPeriod,
108                        troughPeriod, cycleType, rampType, logger, stats);
109                            ex.execute(clientThread);
110                    } 
111            ex.shutdown();
112            // hard time limit of one day for now 
113            // TODO: make this configurable
114            ex.awaitTermination(60 * 60 * 24, TimeUnit.SECONDS);
115            
116            // Log summary statistics for accumulated metrics
117            logger.info(stats.displayOverallSummary());
118            
119            // clean up
120            cleanUp();
121            }
122        
123        protected abstract ClientThread makeClientThread(
124                long iterations, long minDelay, long maxDelay, double sigma,
125                String delayType, long rampPeriod, long peakPeriod,
126                long troughPeriod, String cycleType, String rampType,
127                Logger logger, Statistics stats);
128        
129        /**
130         * This method is invoked by {@link #execute()} after {@link #configure()}
131         * and digester parse, just before client threads are created. Objects that
132         * need to be created and passed to client threads using configuration info
133         * parsed from the config file should be created in this method.
134         * 
135         * @throws Exception
136         */
137        protected void init() throws Exception {}
138        
139        
140        /**
141         * This method is invoked by {@link #execute()} after all spawned threads
142         * have terminated. Override to clean up any resources allocated in 
143         * {@link #init()}.
144         * 
145         * @throws Exception
146         */
147        protected void cleanUp() throws Exception {}
148        
149        
150        /**
151         * Configures basic run parameters. Invoked by Digester via a rule defined
152         * in {@link #configure()}.
153         * 
154         * @param iterations number of iterations
155         * @param clients number of client threads
156         * @param minDelay minimum delay between client thread requests (ms)
157         * @param maxDelay maximum delay between client thread requests (ms)
158         * @param sigma standard deviation of delay
159         * @param delayType type of delay (constant, gaussian, poisson)
160         * @param rampType type of ramp (none, linear, random)
161         * @param rampPeriod rampup/rampdown time
162         * @param peakPeriod peak period
163         * @param troughPeriod trough period
164         * @param cycleType cycle type (none, oscillating)
165         * @throws ConfigurationException
166         */
167        
168        public void configureRun(String iterations, String clients,
169                String minDelay, String maxDelay, String sigma,
170                String delayType, String rampType, String rampPeriod,
171                String peakPeriod, String troughPeriod, String cycleType) 
172                throws ConfigurationException {
173         
174            this.iterations = Long.parseLong(iterations);
175            this.numClients = Long.parseLong(clients);
176            this.minDelay = Long.parseLong(minDelay);
177            this.maxDelay = Long.parseLong(maxDelay);
178            this.sigma = Double.parseDouble(sigma);
179            this.delayType = delayType;
180            this.rampType = rampType;
181            this.rampPeriod = Long.parseLong(rampPeriod);
182            this.peakPeriod = Long.parseLong(peakPeriod);
183            this.troughPeriod = Long.parseLong(troughPeriod);
184            this.cycleType = cycleType;
185            if (cycleType.equals("oscillating") && this.rampPeriod <= 0) {
186                throw new ConfigurationException(
187                  "Ramp period must be positive for oscillating cycle type");
188            }
189        }
190        
191        /**
192         * <p>Starts preparing Digester to parse the configuration file, pushing
193         * *this onto the stack and loading rules to configure basic "run" 
194         * parameters.
195         * </p>
196         * <p>Subclasses can override this, using <code>super()</code> to load base
197         * parameters and then adding additional </code>addCallMethod</code>
198         * sequences for additional parameters.
199         * </p>
200         * 
201         * @throws Exception
202         */
203        protected void configure() throws Exception {
204            digester.push(this);
205            
206            digester.addCallMethod("configuration/run", 
207                    "configureRun", 11);
208            digester.addCallParam(
209                    "configuration/run/iterations", 0);
210            digester.addCallParam(
211                    "configuration/run/clients", 1);
212            digester.addCallParam(
213                    "configuration/run/delay-min", 2);
214            digester.addCallParam(
215                    "configuration/run/delay-max", 3);
216            digester.addCallParam(
217                    "configuration/run/delay-sigma", 4);
218            digester.addCallParam(
219                    "configuration/run/delay-type", 5);
220            digester.addCallParam(
221                    "configuration/run/ramp-type", 6);
222            digester.addCallParam(
223                    "configuration/run/ramp-period", 7);
224            digester.addCallParam(
225                    "configuration/run/peak-period", 8);
226            digester.addCallParam(
227                    "configuration/run/trough-period", 9);
228            digester.addCallParam(
229                    "configuration/run/cycle-type", 10);    
230        }
231        
232        protected void parseConfigFile() throws Exception {
233            // TODO: get rid of File spec
234            digester.parse(new File(configFile));
235        }
236    
237        /**
238         * @return the configFile
239         */
240        public String getConfigFile() {
241            return configFile;
242        }
243    
244        /**
245         * @param configFile the configFile to set
246         */
247        public void setConfigFile(String configFile) {
248            this.configFile = configFile;
249        }
250    
251        /**
252         * @return the digester
253         */
254        public Digester getDigester() {
255            return digester;
256        }
257        
258        /**
259         * @return statistics
260         */
261        public Statistics getStatistics() {
262            return stats;
263        }
264    }