/*
* JBoss, Home of Professional Open Source
* Copyright 2011, Red Hat, Inc. and/or its affiliates, and individual
* contributors as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a full listing
* of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU General Public License, v. 2.0.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License,
* v. 2.0 along with this distribution; if not, write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.mobicents.protocols.smpp.load;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
import com.cloudhopper.commons.charset.CharsetUtil;
import com.cloudhopper.commons.util.DecimalUtil;
import com.cloudhopper.smpp.PduAsyncResponse;
import com.cloudhopper.smpp.SmppBindType;
import com.cloudhopper.smpp.SmppConstants;
import com.cloudhopper.smpp.SmppSession;
import com.cloudhopper.smpp.SmppSessionConfiguration;
import com.cloudhopper.smpp.impl.DefaultSmppClient;
import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
import com.cloudhopper.smpp.pdu.SubmitSm;
import com.cloudhopper.smpp.type.Address;
import com.google.common.util.concurrent.RateLimiter;
/**
* @author amit bhayani
*
*/
public class Client extends TestHarness {
private static final Logger logger = Logger.getLogger(Client.class);
//
// performance testing options (just for this sample)
//
// total number of sessions (conns) to create
private int sessionCount = 5;
// size of window per session
private int windowSize = 50000;
// total number of submit to send total across all sessions
private int submitToSend = 100000;
// total number of submit sent
private volatile AtomicInteger submitSent = new AtomicInteger(0);
//Number of SMPP to submit per sec
private int rateLimiter = 100;
private long startDestNumber = 9960200000l;
private int destNumberDiff = 10000;
private long endDestNumber = startDestNumber + destNumberDiff;
private String sourceNumber = "6666";
private String peerAddress = "127.0.0.1";
private int peerPort = 2775;
private String systemId = "test";
private String password = "test";
private static String message = "Hello world!";
// pause delay after last throttled message in milliseconds
//private static int throttledPause = 1000;
private RateLimiter rateLimiterObj = null;
// 0 - Default MC Mode (e.g. Store and Forward)
// 1 - Datagram mode
// 2 - Forward (i.e. Transaction) mode
// 3 - Store and Forward mode
private static int esmClass = 3;
// private static Date lastThrottledMessageTime;
private static AtomicInteger throttledMessageCount = new AtomicInteger(0);
// total number of submit sent
static public final AtomicInteger SUBMIT_SENT = new AtomicInteger(0);
static public final AtomicInteger SUBMIT_RESP = new AtomicInteger(0);
static long min_dest_number = 9960200000l;
static int dest_number_diff = 100000;
static long max_dest_number = min_dest_number + dest_number_diff;
static public void main(String[] args) throws Exception {
Client client = new Client();
client.test(args);
}
private void test(String[] args) throws Exception {
this.sessionCount = Integer.parseInt(args[0]);
this.windowSize = Integer.parseInt(args[1]);
this.submitToSend = Integer.parseInt(args[2]);
this.rateLimiter = Integer.parseInt(args[3]);
this.startDestNumber = Long.parseLong(args[4]);
this.destNumberDiff = Integer.parseInt(args[5]);
this.sourceNumber = args[6];
this.peerAddress = args[7];
this.peerPort = Integer.parseInt(args[8]);
this.systemId = args[9];
this.password = args[10];
message = args[11];
esmClass = Integer.parseInt(args[12]);
if (sessionCount < 1) {
throw new Exception("Session count cannot be less than 1");
}
if (windowSize < 1) {
throw new Exception("Windows size cannot be less than 1");
}
if (submitToSend < 1) {
throw new Exception("Submit to send cannot be less than 1");
}
if (startDestNumber < 1) {
throw new Exception("Start Destination Number cannot be less than 1");
}
if (destNumberDiff < 1) {
throw new Exception("Destination Number difference cannot be less than 1");
}
if (this.sourceNumber == null || this.sourceNumber == "") {
throw new Exception("Source Number cannot be null");
}
if (this.peerAddress == null || this.peerAddress == "") {
throw new Exception("Peer address cannot be null");
}
if (this.peerPort < 1) {
throw new Exception("Peer port cannot be less than 1");
}
if (this.message == null) {
throw new Exception("Message cannot be less than 1");
}
this.endDestNumber = startDestNumber + destNumberDiff;
logger.info("sessionCount=" + sessionCount);
logger.info("windowSize=" + windowSize);
logger.info("submitToSend=" + submitToSend);
logger.info("startDestNumber=" + startDestNumber);
logger.info("destNumberDiff=" + destNumberDiff);
logger.info("endDestNumber=" + endDestNumber);
logger.info("sourceNumber=" + sourceNumber);
logger.info("peerAddress=" + peerAddress);
logger.info("peerPort=" + peerPort);
logger.info("systemId=" + systemId);
logger.info("password=" + password);
logger.info("message=" + message);
logger.info("rateLimiter=" + rateLimiter + " sms/sec");
this.rateLimiterObj = RateLimiter.create(this.rateLimiter); // rate
// lastThrottledMessageTime = null;
//
// setup 3 things required for any session we plan on creating
//
// for monitoring thread use, it's preferable to create your own
// instance
// of an executor with Executors.newCachedThreadPool() and cast it to
// ThreadPoolExecutor
// this permits exposing thinks like executor.getActiveCount() via JMX
// possible
// no point renaming the threads in a factory since underlying Netty
// framework does not easily allow you to customize your thread names
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
// to enable automatic expiration of requests, a second scheduled
// executor
// is required which is what a monitor task will be executed with - this
// is probably a thread pool that can be shared with between all client
// bootstraps
ScheduledThreadPoolExecutor monitorExecutor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1,
new ThreadFactory() {
private AtomicInteger sequence = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("SmppClientSessionWindowMonitorPool-" + sequence.getAndIncrement());
return t;
}
});
// a single instance of a client bootstrap can technically be shared
// between any sessions that are created (a session can go to any
// different
// number of SMSCs) - each session created under
// a client bootstrap will use the executor and monitorExecutor set
// in its constructor - just be *very* careful with the
// "expectedSessions"
// value to make sure it matches the actual number of total concurrent
// open sessions you plan on handling - the underlying netty library
// used for NIO sockets essentially uses this value as the max number of
// threads it will ever use, despite the "max pool size", etc. set on
// the executor passed in here
DefaultSmppClient clientBootstrap = new DefaultSmppClient(Executors.newCachedThreadPool(), this.sessionCount,
monitorExecutor);
// same configuration for each client runner
SmppSessionConfiguration config = new SmppSessionConfiguration();
config.setWindowSize(this.windowSize);
config.setName("Tester.Session.0");
config.setType(SmppBindType.TRANSCEIVER);
// config.setHost("107.178.220.137");
config.setHost(this.peerAddress);
config.setPort(this.peerPort);
config.setConnectTimeout(10000);
config.setSystemId(this.systemId);
config.setPassword(this.password);
config.getLoggingOptions().setLogBytes(false);
// to enable monitoring (request expiration)
config.setRequestExpiryTimeout(30000);
config.setWindowMonitorInterval(15000);
config.setCountersEnabled(true);
// config.setAddressRange("6666");
// various latches used to signal when things are ready
CountDownLatch allSessionsBoundSignal = new CountDownLatch(this.sessionCount);
CountDownLatch startSendingSignal = new CountDownLatch(1);
// create all session runners and executors to run them
ThreadPoolExecutor taskExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
ClientSessionTask[] tasks = new ClientSessionTask[this.sessionCount];
for (int i = 0; i < this.sessionCount; i++) {
tasks[i] = new ClientSessionTask(allSessionsBoundSignal, startSendingSignal, clientBootstrap, config,
this.submitToSend, this.rateLimiterObj);
taskExecutor.submit(tasks[i]);
}
// wait for all sessions to bind
logger.info("Waiting up to 7 seconds for all sessions to bind...");
if (!allSessionsBoundSignal.await(7000, TimeUnit.MILLISECONDS)) {
throw new Exception("One or more sessions were unable to bind, cancelling test");
}
logger.info("Sending signal to start test...");
long startTimeMillis = System.currentTimeMillis();
startSendingSignal.countDown();
// wait for all tasks to finish
taskExecutor.shutdown();
taskExecutor.awaitTermination(3, TimeUnit.DAYS);
long stopTimeMillis = System.currentTimeMillis();
// did everything succeed?
int actualSubmitSent = 0;
int sessionFailures = 0;
for (int i = 0; i < this.sessionCount; i++) {
if (tasks[i].getCause() != null) {
sessionFailures++;
logger.error("Task #" + i + " failed with exception: " + tasks[i].getCause());
} else {
actualSubmitSent += tasks[i].getSubmitRequestSent();
}
}
// actualSubmitSent -= throttledMessageCount.get();
logger.info("Performance client finished:");
logger.info(" Sessions: " + this.sessionCount);
logger.info(" Window Size: " + this.windowSize);
logger.info("Sessions Failed: " + sessionFailures);
logger.info(" Time: " + (stopTimeMillis - startTimeMillis) + " ms");
logger.info(" Target Submit: " + this.submitToSend);
logger.info(" Actual Submit: " + actualSubmitSent);
logger.info(" Throttled Message count: " + throttledMessageCount);
double throughput = (double) actualSubmitSent / ((double) (stopTimeMillis - startTimeMillis) / (double) 1000);
logger.info(" Throughput: " + DecimalUtil.toString(throughput, 3) + " per sec");
for (int i = 0; i < this.sessionCount; i++) {
if (tasks[i].session != null && tasks[i].session.hasCounters()) {
logger.info(" Session " + i + ": submitSM " + tasks[i].session.getCounters().getTxSubmitSM());
}
}
// this is required to not causing server to hang from non-daemon
// threads
// this also makes sure all open Channels are closed to I *think*
logger.info("Shutting down client bootstrap and executors...");
clientBootstrap.destroy();
executor.shutdownNow();
monitorExecutor.shutdownNow();
logger.info("Done. Exiting");
}
public static class ClientSessionTask implements Runnable {
private SmppSession session;
private CountDownLatch allSessionsBoundSignal;
private CountDownLatch startSendingSignal;
private DefaultSmppClient clientBootstrap;
private SmppSessionConfiguration config;
private int submitRequestSent;
private int submitResponseReceived;
private AtomicBoolean sendingDone;
private Exception cause;
private Random r = new Random();
private int submitToSend;
private CountDownLatch allSubmitResponseReceivedSignal;
private RateLimiter rateLimiterObj;
public ClientSessionTask(CountDownLatch allSessionsBoundSignal, CountDownLatch startSendingSignal,
DefaultSmppClient clientBootstrap, SmppSessionConfiguration config, int submitToSend, RateLimiter rateLimiterObj) {
this.allSessionsBoundSignal = allSessionsBoundSignal;
this.startSendingSignal = startSendingSignal;
this.clientBootstrap = clientBootstrap;
this.config = config;
this.submitRequestSent = 0;
this.submitResponseReceived = 0;
this.sendingDone = new AtomicBoolean(false);
this.submitToSend = submitToSend;
this.rateLimiterObj = rateLimiterObj;
}
public Exception getCause() {
return this.cause;
}
public int getSubmitRequestSent() {
return this.submitRequestSent;
}
@Override
public void run() {
// a countdownlatch will be used to eventually wait for all
// responses to be received by this thread since we don't want to
// exit too early
allSubmitResponseReceivedSignal = new CountDownLatch(1);
DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler();
String text160 = message;
byte[] textBytes = CharsetUtil.encode(text160, CharsetUtil.CHARSET_GSM);
try {
// create session a session by having the bootstrap connect a
// socket, send the bind request, and wait for a bind response
session = clientBootstrap.bind(config, sessionHandler);
// don't start sending until signalled
allSessionsBoundSignal.countDown();
startSendingSignal.await();
// all threads compete for processing
while (true) {
// if (lastThrottledMessageTime != null) {
// long passedTime = (new Date()).getTime() -
// lastThrottledMessageTime.getTime();
// if (passedTime < throttledPause) {
// Thread.sleep(throttledPause - passedTime + 50);
// continue;
// }
// }
if (SUBMIT_SENT.get() >= this.submitToSend) {
if (allSubmitResponseReceivedSignal.await(100, TimeUnit.MILLISECONDS)) {
break;
}
if (SUBMIT_SENT.getAndIncrement() >= this.submitToSend)
continue;
}
this.rateLimiterObj.acquire();
SubmitSm submit = new SubmitSm();
submit.setSourceAddress(new Address((byte) 0x01, (byte) 0x01, "6666"));
long destination = r.nextInt(dest_number_diff) + min_dest_number;
submit.setDestAddress(new Address((byte) 0x01, (byte) 0x01, Long.toString(destination)));
submit.setShortMessage(textBytes);
submit.setEsmClass((byte) esmClass);
// asynchronous send
this.submitRequestSent++;
sendingDone.set(true);
session.sendRequestPdu(submit, 30000, false);
SUBMIT_SENT.getAndIncrement();
}
// all threads have sent all submit, we do need to wait for
// an acknowledgement for all "inflight" though (synchronize
// against the window)
logger.info("before waiting sendWindow.size: " + session.getSendWindow().getSize());
logger.info("Final Session rx-submitSM" + session.getCounters().getRxSubmitSM());
logger.info("Final Session tx-submitSM" + session.getCounters().getTxSubmitSM());
// allSubmitResponseReceivedSignal.await();
logger.info("after waiting sendWindow.size: " + session.getSendWindow().getSize());
session.unbind(5000);
} catch (Exception e) {
logger.error("", e);
this.cause = e;
}
}
class ClientSmppSessionHandler extends DefaultSmppSessionHandler {
public ClientSmppSessionHandler() {
}
@Override
public void fireChannelUnexpectedlyClosed() {
// this is an error we didn't really expect for perf testing
// its best to at least countDown the latch so we're not waiting
// forever
logger.error("Unexpected close occurred...");
allSubmitResponseReceivedSignal.countDown();
}
@Override
public void fireExpectedPduResponseReceived(PduAsyncResponse pduAsyncResponse) {
if (pduAsyncResponse.getResponse().getCommandStatus() == SmppConstants.STATUS_THROTTLED) {
// lastThrottledMessageTime = new Date();
SUBMIT_SENT.decrementAndGet();
throttledMessageCount.incrementAndGet();
submitRequestSent--;
} else {
submitResponseReceived++;
SUBMIT_RESP.incrementAndGet();
// if the sending thread is finished, check if we're done
// if (sendingDone.get()) {
if (SUBMIT_SENT.get() >= submitToSend) {
if (submitResponseReceived >= submitRequestSent) { // submitToSend
allSubmitResponseReceivedSignal.countDown();
}
}
}
}
}
}
}