package org.marketcetera.modules.async;
import org.marketcetera.util.misc.ClassVersion;
import org.marketcetera.module.*;
import org.marketcetera.core.LoggerConfiguration;
import static org.marketcetera.modules.async.SimpleAsyncProcessorFactory.PROVIDER_URN;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.After;
import org.junit.Test;
import static org.junit.Assert.*;
import org.hamcrest.Matchers;
import javax.management.*;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.math.BigDecimal;
/* $License$ */
/**
* Tests the {@link SimpleAsyncProcessor} module.
*
* @author anshul@marketcetera.com
* @version $Id: AsyncModuleTest.java 16841 2014-02-20 19:59:04Z colin $
* @since 2.0.0
*/
@ClassVersion("$Id: AsyncModuleTest.java 16841 2014-02-20 19:59:04Z colin $")
public class AsyncModuleTest extends ModuleTestBase {
@BeforeClass
public static void logSetup() {
LoggerConfiguration.logSetup();
}
/**
* Verifies the provider and module infos.
*
* @throws Exception if there were unexpected errors
*/
@Test
public void info() throws Exception {
//Provider Info
assertProviderInfo(mManager, PROVIDER_URN,
new String[]{ModuleURN.class.getName()}, new Class[]{ModuleURN.class},
Messages.PROVIDER_DESCRIPTION.getText(),
true, true);
//Create a module and test its info
ModuleURN instanceURN = new ModuleURN(PROVIDER_URN, "mymodule");
assertEquals(instanceURN, mManager.createModule(
PROVIDER_URN,
instanceURN));
assertModuleInfo(mManager, instanceURN, ModuleState.STARTED, null,
null, false, true, true, true, false);
//verify that the module has no flow attributes.
assertTrue(getAttributes(instanceURN).isEmpty());
//Stop and Delete the module
mManager.stop(instanceURN);
mManager.deleteModule(instanceURN);
}
/**
* Verifies what kinds of request parameters (null) can be supplied to the
* module when initiating data flow requests.
*
* @throws Exception if there was an error.
*/
@Test
public void requestParameters() throws Exception {
final ModuleURN instanceURN = new ModuleURN(PROVIDER_URN, "mymodule");
final String requestParm = "not null value";
new ExpectedFailure<IllegalRequestParameterValue>(
org.marketcetera.module.Messages.ILLEGAL_REQ_PARM_VALUE,
instanceURN.getValue(), requestParm) {
public void run() throws Exception {
mManager.createDataFlow(new DataRequest[]{
new DataRequest(CopierModuleFactory.INSTANCE_URN,
"doesnt matter"),
new DataRequest(instanceURN, requestParm)
});
}
};
//Verify that the module instance is not leaked.
List<ModuleURN> instances = mManager.getModuleInstances(PROVIDER_URN);
assertTrue(instances.toString(), instances.isEmpty());
}
/**
* Verifies a simple data flow.
*
* @throws Exception if there were errors.
*/
@Test
public void simpleFlowAndJMX() throws Exception {
ModuleURN instanceURN = new ModuleURN(PROVIDER_URN, "mymodule");
//Set up the data
Object []requestParm = {BigDecimal.ONE, 2, "three"};
CopierModule.SynchronousRequest req =
new CopierModule.SynchronousRequest(requestParm);
req.semaphore.acquire();
//Set up the sink listener
BlockingSinkDataListener listener = new BlockingSinkDataListener();
mManager.addSinkListener(listener);
//Create the data flow.
DataFlowID flowID = mManager.createDataFlow(new DataRequest[]{
new DataRequest(CopierModuleFactory.INSTANCE_URN,
req),
new DataRequest(instanceURN, null)
});
//wait for the data to be emitted.
req.semaphore.acquire();
//wait for the data to be received
for(Object o: requestParm) {
assertEquals(o, listener.getNextData());
}
//verify the flow info
DataFlowInfo flowInfo = mManager.getDataFlowInfo(flowID);
assertFlowInfo(flowInfo, flowID, 3, true, false, null, null);
//verify the last 2 flow steps
assertFlowStep(flowInfo.getFlowSteps()[1], instanceURN, true, 3, 0,
null, true, 3, 0, null, instanceURN, null);
assertFlowStep(flowInfo.getFlowSteps()[2],
SinkModuleFactory.INSTANCE_URN, false, 0, 0, null, true, 3, 0,
null, SinkModuleFactory.INSTANCE_URN, null);
//verify the module info to double check that it's auto-created.
assertModuleInfo(mManager, instanceURN, ModuleState.STARTED, null,
new DataFlowID[]{flowID}, true, true, true, true, false);
//verify JMX MXBean Info
final ObjectName on = instanceURN.toObjectName();
final MBeanServer beanServer = getMBeanServer();
assertTrue(beanServer.isRegistered(on));
MBeanInfo beanInfo = beanServer.getMBeanInfo(on);
assertEquals(SimpleAsyncProcessor.class.getName(), beanInfo.getClassName());
assertEquals(Messages.JMX_MXBEAN_DESCRIPTION.getText(), beanInfo.getDescription());
assertEquals(0, beanInfo.getOperations().length);
assertEquals(0, beanInfo.getConstructors().length);
assertEquals(0, beanInfo.getNotifications().length);
assertEquals(0, beanInfo.getDescriptor().getFieldNames().length);
MBeanAttributeInfo[] attributeInfos = beanInfo.getAttributes();
assertEquals(1, attributeInfos.length);
final String validAttribute = SimpleAsyncProcessor.ATTRIB_PREFIX + flowID;
assertEquals(validAttribute, attributeInfos[0].getName());
assertEquals(Integer.class.getName(), attributeInfos[0].getType());
assertEquals(Messages.JMX_ATTRIBUTE_FLOW_CNT_DESCRIPTION.getText(flowID), attributeInfos[0].getDescription());
assertEquals(0, attributeInfos[0].getDescriptor().getFieldNames().length);
assertFalse(attributeInfos[0].isIs());
assertFalse(attributeInfos[0].isWritable());
assertTrue(attributeInfos[0].isReadable());
//verify Attributes
Object value = beanServer.getAttribute(on, SimpleAsyncProcessor.ATTRIB_PREFIX + flowID);
assertEquals((Integer)0, (Integer)value);
final String invalidAttribute = SimpleAsyncProcessor.ATTRIB_PREFIX + 1;
new ExpectedFailure<AttributeNotFoundException>(invalidAttribute){
@Override
protected void run() throws Exception {
beanServer.getAttribute(on, invalidAttribute);
}
};
new ExpectedFailure<AttributeNotFoundException>("blah"){
@Override
protected void run() throws Exception {
beanServer.getAttribute(on, "blah");
}
};
AttributeList attribList = beanServer.getAttributes(on,
new String[]{validAttribute, invalidAttribute});
assertEquals(1, attribList.size());
assertEquals(new Attribute(validAttribute, 0), attribList.get(0));
new ExpectedFailure<AttributeNotFoundException>(){
@Override
protected void run() throws Exception {
beanServer.setAttribute(on, new Attribute(validAttribute, 34));
}
};
new ExpectedFailure<AttributeNotFoundException>(){
@Override
protected void run() throws Exception {
beanServer.setAttribute(on, new Attribute(invalidAttribute, 34));
}
};
AttributeList list = new AttributeList(Arrays.asList(
new Attribute(validAttribute, 12),
new Attribute(invalidAttribute, 13)
));
assertEquals(0, beanServer.setAttributes(on, list).size());
//verify no operations can be performed
ReflectionException excpt = new ExpectedFailure<ReflectionException>() {
@Override
protected void run() throws Exception {
beanServer.invoke(on, "getQueueSizes", null, null);
}
}.getException();
assertTrue(excpt.toString(), excpt.getCause() instanceof NoSuchMethodException);
//stop the flow
mManager.cancel(flowID);
//verify that the module is deleted.
List<ModuleURN> instances = mManager.getModuleInstances(PROVIDER_URN);
assertTrue(instances.toString(), instances.isEmpty());
//remove the listener
mManager.removeSinkListener(listener);
}
/**
* Verifies that the async module, when participating in multiple data flows,
* only emits data into respective data flows.
*
* @throws Exception if there were errors.
*/
@Test
public void noEmitFromOtherFlows() throws Exception {
ModuleURN instanceURN = new ModuleURN(PROVIDER_URN, "mymodule");
FlowSpecificListener listener = new FlowSpecificListener();
mManager.addSinkListener(listener);
//Set up a data flow with this module as an emitter.
//No data should be received from within this data flow
DataFlowID emitOnlyflowID = mManager.createDataFlow(new DataRequest[]{
new DataRequest(instanceURN)
});
//Setup two other data flows
Object[] reqParms1 = {"firstOne", BigDecimal.TEN, "uno"};
Object[] reqParms2 = {"secondOne", BigDecimal.ZERO, "dos"};
DataFlowID flowID1 = mManager.createDataFlow(new DataRequest[]{
new DataRequest(CopierModuleFactory.INSTANCE_URN, reqParms1),
new DataRequest(instanceURN)
});
DataFlowID flowID2 = mManager.createDataFlow(new DataRequest[]{
new DataRequest(CopierModuleFactory.INSTANCE_URN, reqParms2),
new DataRequest(instanceURN)
});
//Wait for data from each of the flows
//flowID2
while(!listener.getFlows().contains(flowID2)) {
Thread.sleep(100);
}
for(Object o: reqParms2) {
assertEquals(o, listener.getNextDataFor(flowID2));
}
//flowID1
while(!listener.getFlows().contains(flowID1)) {
Thread.sleep(100);
}
for(Object o: reqParms1) {
assertEquals(o, listener.getNextDataFor(flowID1));
}
//No data should be received for the very first data flow
assertThat(listener.getFlows(), Matchers.not(Matchers.hasItem(emitOnlyflowID)));
//verify queue sizes for all of them via JMX
assertEquals(0, getMBeanServer().getAttribute(instanceURN.toObjectName(), SimpleAsyncProcessor.ATTRIB_PREFIX + emitOnlyflowID));
assertEquals(0, getMBeanServer().getAttribute(instanceURN.toObjectName(), SimpleAsyncProcessor.ATTRIB_PREFIX + flowID1));
assertEquals(0, getMBeanServer().getAttribute(instanceURN.toObjectName(), SimpleAsyncProcessor.ATTRIB_PREFIX + flowID2));
//verify that the data for each flow is delivered in a unique thread.
assertEquals(null, listener.getThreadNameFor(emitOnlyflowID));
assertThat(listener.getThreadNameFor(flowID1),
Matchers.startsWith(SimpleAsyncProcessor.ASYNC_THREAD_NAME_PREFIX + "-" + instanceURN.instanceName()));
assertThat(listener.getThreadNameFor(flowID2),
Matchers.startsWith(SimpleAsyncProcessor.ASYNC_THREAD_NAME_PREFIX + "-" + instanceURN.instanceName()));
assertThat(listener.getThreadNameFor(flowID1), Matchers.not(
Matchers.equalTo(listener.getThreadNameFor(flowID2))));
assertThat(listener.getThreadNameFor(flowID1), Matchers.not(
Matchers.equalTo(Thread.currentThread().getName())));
//Cancel all the data flows
mManager.cancel(emitOnlyflowID);
mManager.cancel(flowID1);
//verify that we only have a single flow attribute left
List<String> list = getAttributes(instanceURN);
assertEquals(1, list.size());
assertThat(list, Matchers.hasItem(SimpleAsyncProcessor.ATTRIB_PREFIX + flowID2));
mManager.cancel(flowID2);
//verify that the module is deleted.
List<ModuleURN> instances = mManager.getModuleInstances(PROVIDER_URN);
assertTrue(instances.toString(), instances.isEmpty());
mManager.removeSinkListener(listener);
}
/**
* Verifies that a really slow consumer of events does not slow down
* the emitter of events when the event is delivered via the async module.
*
* @throws Exception if there were exceptions
*/
@Test(timeout = 10000)
public void slowConsumer() throws Exception {
ModuleURN instanceURN = new ModuleURN(PROVIDER_URN, "mymodule");
Object [] data = {"item1", "item2", "item3", "item4"};
DataFlowID flowID = mManager.createDataFlow(new DataRequest[]{
new DataRequest(CopierModuleFactory.INSTANCE_URN, data),
new DataRequest(instanceURN),
new DataRequest(BlockingModuleFactory.INSTANCE_URN)
});
//wait until copier is done emitting all the data
DataFlowInfo flowInfo;
do {
Thread.sleep(100);
flowInfo = mManager.getDataFlowInfo(flowID);
} while(flowInfo.getFlowSteps()[0].getNumEmitted() < 4);
for(int i = 0; i < data.length; i++) {
//wait for the data to get delivered
BlockingModuleFactory.getLastInstance().getSemaphore().acquire();
//verify the flow info for the last step
flowInfo = mManager.getDataFlowInfo(flowID);
assertFlowStep(flowInfo.getFlowSteps()[2], BlockingModuleFactory.INSTANCE_URN,
false, 0, 0, null, true, i + 1, 0, null, BlockingModuleFactory.INSTANCE_URN, null);
//verify the jmx flow queue size attribute value
if(i < data.length - 1) {
assertEquals(data.length - 1 - i,
getMBeanServer().getAttribute(instanceURN.toObjectName(),
SimpleAsyncProcessor.ATTRIB_PREFIX + flowID));
}
//consume the data
assertEquals(data[i], BlockingModuleFactory.getLastInstance().getNextData());
}
//verify that the queue size is now zero.
assertEquals(0, getMBeanServer().getAttribute(
instanceURN.toObjectName(),
SimpleAsyncProcessor.ATTRIB_PREFIX + flowID));
//cancel data flow
mManager.cancel(flowID);
}
/**
* Returns the list of attributes exposed by the mbean having
* the supplied URN.
*
* @param inURN the module URN.
*
* @return the list of advertised attributes.
*
* @throws Exception if there were exceptions.
*/
private List<String> getAttributes(ModuleURN inURN) throws Exception {
MBeanInfo beanInfo = getMBeanServer().getMBeanInfo(inURN.toObjectName());
List<String> attribs = new ArrayList<String>();
for(MBeanAttributeInfo info: beanInfo.getAttributes()) {
attribs.add(info.getName());
}
return attribs;
}
/**
* A sinkd data listener that saves data received for each data flow
* into a separate queue. For each data flow, each object delivered and
* the number of objects available can be queried.
* The class also records the thread that was used to deliver the first
* data item for each data flow.
*/
private static class FlowSpecificListener implements SinkDataListener {
@Override
public void receivedData(DataFlowID inFlowID, Object inData) {
BlockingQueue<Object> queue = mFlowData.get(inFlowID);
if(queue == null) {
synchronized(this) {
queue = mFlowData.get(inFlowID);
if(queue == null) {
queue = new LinkedBlockingQueue<Object>();
mFlowData.put(inFlowID, queue);
mThreadNames.put(inFlowID, Thread.currentThread().getName());
}
}
}
queue.add(inData);
}
/**
* Returns the next data delivered to the listener for the supplied
* data flow ID.
*
* @param inFlowID the data flow ID.
*
* @return the next data item.
*
* @throws InterruptedException if the wait for receiving the next
* data item was interrupted.
*/
public Object getNextDataFor(DataFlowID inFlowID) throws InterruptedException {
BlockingQueue<Object> queue = mFlowData.get(inFlowID);
return queue == null
? null
: queue.take();
}
/**
* The name of the thread that delivered the first data iterm for
* the specified flowID.
*
* @param inFlowID the flowID.
*
* @return the name of the thread that delivered the first data item
* for the flow with the specified flowID.
*/
public String getThreadNameFor(DataFlowID inFlowID) {
synchronized (this) {
return mThreadNames.get(inFlowID);
}
}
/**
* The set of data flows that have delivered data to this listener.
*
* @return the set of IDs of data flows that have delivered data to
* this listener.
*/
public Set<DataFlowID> getFlows() {
return mFlowData.keySet();
}
private final Map<DataFlowID, BlockingQueue<Object>> mFlowData =
new ConcurrentHashMap<DataFlowID, BlockingQueue<Object>>();
private final Map<DataFlowID, String> mThreadNames =
new HashMap<DataFlowID, String>();
}
@Before
public void setup() throws Exception {
mManager = new ModuleManager();
mManager.init();
}
@After
public void cleanup() throws Exception {
mManager.stop();
mManager = null;
}
private ModuleManager mManager;
}