Share This:

Writing BAO Adapters - Index

 

Introduction

In Part 4 we put together a working, though simplistic, actor adapter. Now we move on to the monitor adapter. This adapter type is effectively the reverse of the actor; where the actor allows us to effect change in other systems, the monitor allows change in other systems to affect us.

 

Special thanks to Ranganath Samudrala for the simple monitor adapter example he sent me some time back, which really helped my understanding of how monitor adapters are constructed.

 

Our example monitor adapter

As we did with the actor adapter before, we’ll first implement a very basic skeleton that loads on the grid and generates a simple event. It turns out this is slightly trickier with the monitor adapter since it must have an active execution loop running in order for the adapter to stay running on the grid.

 

In the next article the adapter will be extended to send events based on a real external change, but for now we'll simply send an event to the grid based on a timer; in other words, generate an event every x milliseconds.

 

Configuration Parameters

As with the actor adapters, each named monitor adapter on the grid has an XML configuration document that allows the user to set global parameters for that adapter. Unlike actors, however, there are no adapter requests in which these parameters can be overridden.

 

We’ll include a single configuration parameter in our basic adaper: sleep-delay. This will be the number of milliseconds between events and will default to 5000, which will fire off an event every five seconds.

 

WARNING: There is no checking for ‘sensible’ values here, so if you set this to a value of 1 millisecond your adapter is going to flood the grid with events. This is not likely to make your grid peers very happy...

 

The only required method for the monitor adapter configuration (in addition to the constructor) is validate(). This method is called when BAO wishes to check that the configuration is valid; if you find any configuration parameters that are not suitable, you throw an InvalidConfigurationException.

 

The Monitor Adapter Life-cycle

The life-cycle of the monitor adapter is very much the same as that of the actor adapter if you look at it in terms of its operational states (stopped, started, and so on). There is a significant implementation difference, though; where an actor adapter only executes when it is passed an adapter request, an enabled monitor adapter is running all the time. It may spend much of this time in an idle loop, or waiting for an inbound socket connection, or polling a file.

 

Here's an extended version of the adapter life-cycle diagram seen in an earlier article:

 

Adapter States - extended.png

 

The official development documentation for AO doesn't make clear the purpose of all of the adapter states, so a little bit of interpretation and invention is needed to establish how best to use them. The two that you must use are RUNNING and STOPPED. The others aren't critical but let's call it good practice to use them. I would propose:

 

INITIALIZING: Set this as early as possible in your initialize() method.

STARTING: Set this at the end of your initialize() method.

RUNNING: Set this at the start of your run() method.

STOPPING: Set this at the start of your shutdown() method.

STOPPED: Set this at the end of your shutdown() method.

FAULT: Generally it seems better to generate an appropriate exception to indicate a fault (e.g. InvalidConfigurationException), but there are certainly cases where we might wish to move the adapter to a fault state that aren't covered by the standard adapter exception. An example might be that the target system has gone offline.

PAUSED: Ignore this one for now; at the moment I can't see that pause functionality even exists as part of the adapter definition, so this is one to come back to in a future article .

 

Event De-duplication

The grid performs a very important function in relation to monitor adapters, and that is de-duplication of events. This is mainly useful when you have multiple peers running the same adapter for resilience, and those adapters are likely to generate the same event in response to some single external change. The grid peers generate a hash of each received event and store this in a queue. After a short time (2 minutes by default), each hash expires from the queue and is removed. Every incoming event is checked against this queue, and if its hash matches an existing entry then the event is discarded.

 

This is why our “dummy” event includes a counter, otherwise the majority of our events would be discarded as they’d be identical to the first one recorded on the queue. After two minutes, that first entry expires and another identical event would be allowed through.

 

So be aware of de-duplication and how it can work both for you and against you. If you are missing events, chances are you’re sending multiple identical events within two minutes of each other. If you are getting more events than expected, you may have introduced some unintended “uniqueness” to your events that is preventing them from being de-duplicated.

 

Gotta Have Rules

All events your adapter sends to the adapter manager are then pushed to the Activity Processor for rules processing. All rules present in active modules are evaluated and matching rules result in the execution of the associated workflow, with the event passed in the context item “inputevent”.

 

Note that it is entirely possible for multiple rules to match the event, and hence multiple workflows will fire based on that event.

 

Required Methods

There are a few mandatory methods for monitor adapters:

public void run()

public void shutdown()

 

The run() method is the “main loop” of the adapter that listens, polls, or performs whatever our monitoring action happens to be. If you need an adapter that monitors a tiger by poking it with a stick, this is the method in which to implement that.

 

As might be expected, shutdown() should contain any logic required to disconnect from external systems or otherwise clean up as the adapter is being stopped.

 

The Configuration Class

The basic configuration class is not so different from the one used with the actor adapter:

 

package com.example.bao.adapter.timedmonitor.Configuration;

import com.example.bao.adapter.timedmonitor.TimedMonitorAdapter;
import com.realops.common.configuration.InvalidConfigurationException;
import com.realops.foundation.adapterframework.configuration.AdapterConfigurationException;
import com.realops.foundation.adapterframework.configuration.BaseAdapterConfiguration;

import java.util.Hashtable;
import java.util.Set;

/**
* This is the configuration class for the Timed Monitor adapter (see {@link TimedMonitorAdapter}).
*
 * @author      Gordon Mckeown <gordon_mckeown@bmc.com>
* @version     1.0
*
*/
public class TimedMonitorAdapterConfig extends BaseAdapterConfiguration {
    private static final String CONFIG_SLEEP_DELAY = "sleep-delay";
    private static final long DEFAULT_SLEEP_DELAY = 5000;

    /**
     * Class constructor taking the ID of the adapter being instantiated.
     *
     * @param adapterId     String uniquely identifying this instance of the adapter
     */
    public TimedMonitorAdapterConfig(String adapterId) {
        super(adapterId);
        addRequiredKeys();
        addValidKeys();
    }

    /**
     * Class constructor taking the ID of the adapter and a hashtable of default options
     *
     * @param id            String uniquely identifying this instance of the adapter
     * @param defaults
     */
    public TimedMonitorAdapterConfig(String id, Hashtable defaults) {
        super(id, defaults);
        addRequiredKeys();
        addValidKeys();
    }

    /**
     * Class constructor taking the ID of the adapter, a hash-table of default options, and sets defining the valid and required keys.
     *
     * @param id            String uniquely identifying this instance of the adapter
     * @param defaults
     * @param validKeys
     * @param requiredKeys
     * @throws AdapterConfigurationException
     */
    public TimedMonitorAdapterConfig(String id, Hashtable defaults, Set validKeys, Set requiredKeys)
            throws AdapterConfigurationException {
        super(id, defaults, validKeys, requiredKeys);
        addRequiredKeys();
        addValidKeys();
    }

    /**
     * Method to advise the adapter manager which configuration entries are mandatory
     */
    private void addRequiredKeys() {
        // Do nothing
    }

    /**
     * Method to advise the adapter manager which configuration entries are permitted.
     */
    private void addValidKeys() {
        this.addValidKey(CONFIG_SLEEP_DELAY);
    }

    /**
     * Method to perform validation checks on the supplied adapter configuration.
     *
     * @throws InvalidConfigurationException
     */
    public void validate() throws InvalidConfigurationException {
        super.validate();
    }

    /**
     * Getter method for the configured delay between events
     * @return  the chosen sleep delay in milliseconds
     */
    public long getSleepDelay() {
        Long sleepDelay = super.getLongProperty(CONFIG_SLEEP_DELAY);
        if (sleepDelay == null || sleepDelay.longValue() == 0) {
            sleepDelay = DEFAULT_SLEEP_DELAY;
        }
        return sleepDelay;
    }
}






 

The Monitor Adapter Class

Here’s the bit you've been waiting for; our skeleton monitor adapter:

 

package com.example.bao.adapter.timedmonitor;

import com.example.bao.adapter.timedmonitor.Configuration.TimedMonitorAdapterConfig;
import com.realops.common.enumeration.StateEnum;
import com.realops.common.xml.XML;
import com.realops.foundation.adapterframework.AbstractMonitorAdapter;
import com.realops.foundation.adapterframework.AdapterException;
import com.realops.foundation.adapterframework.AdapterManager;

/**
* This class defines a minimal framework to get a monitor adapter running on an AO grid.
*
 * @author      Gordon Mckeown <gordon_mckeown@bmc.com>
* @version     1.0
*
*/
public class TimedMonitorAdapter extends AbstractMonitorAdapter {
    private Long sleepDelay;
    private TimedMonitorAdapterConfig adapterConfig;
    private int executionCounter = 0;

    /**
     * Default constructor
     */
    public TimedMonitorAdapter() {
        super();
    }

    /**
     * Initialisation method called when adapter is started by Adapter Manager
     *
     * @param adapterManager
     */
    @Override
    @SuppressWarnings("deprecation")
    public void initialize(AdapterManager adapterManager) {
        super.initialize(adapterManager);
        setState(StateEnum.INITIALIZING);
        adapterConfig = (TimedMonitorAdapterConfig) getConfiguration();
        sleepDelay = adapterConfig.getSleepDelay();
        setState(StateEnum.STARTING);
    }

    /**
     * Returns the class name of our configuration class.
     *
     * @return  Name of configuration class
     */
    @Override
    public String getAdapterConfigurationClassName() {
        return TimedMonitorAdapterConfig.class.getName();
    }

    /**
     * Shutdown method called when adapter is stopped by Adapter Manager
     *
     * @throws AdapterException
     */
    @Override
    @SuppressWarnings("deprecation")
    public void shutdown() throws AdapterException {
        setState(StateEnum.STOPPING);
        try {
            Thread.sleep(sleepDelay);   // Allow main loop time to finish
            setState(StateEnum.STOPPED);
        } catch (InterruptedException e) {
            setState(StateEnum.FAULT);
        }
    }

    /**
     * Main adapter logic loop that runs whilst adapter is in "running" state.
     */
    @Override
    @SuppressWarnings("deprecation")
    public void run() {
        setState(StateEnum.RUNNING);

        while(getState() == StateEnum.RUNNING) {
            executionCounter++;
            XML xmlEvent = new XML("dummy-event");
            xmlEvent.setText(getName());
            this.sendEvent("Dummy event, loop number: " + executionCounter, xmlEvent);
            try {
                Thread.sleep(sleepDelay);
            } catch (InterruptedException e) {
                setState(StateEnum.FAULT);
            }
        }
    }
}





 

A Test Class

Because of the way monitor adapters work – their ‘always running’ nature – our test class needs to be constructed slightly differently. Also, we need a way to receive the event being sent by the adapter, and this involves creating a “mock” adapter manager. Creating an adapter manager object requires an additional library from the tomcat/lib folder on your CDP: servlet-api.jar.

 

Without this library in your class path, you will see “class not found” errors relating to javax/servlet/filter when trying to test. I’ve simply added it to the library folder containing the other hundred-or-so BAO libs.

 

Our example test class isn't massively functional; it simply executes the monitor adapter in a thread for 20 seconds and then shuts it down. No particular checks are performed to ensure the correct operation of the adapter itself. All that said, here’s the test class:

 

package com.example.bao.adapter.timedmonitor;

import com.example.bao.adapter.timedmonitor.Configuration.TimedMonitorAdapterConfig;
import com.realops.common.configuration.InvalidConfigurationException;
import com.realops.common.enumeration.StateEnum;
import com.realops.common.util.ResourceLocator;
import com.realops.common.xml.InvalidXMLFormatException;
import com.realops.common.xml.XML;
import com.realops.foundation.adapterframework.AdapterManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import static org.junit.Assert.*;

/**
* Test class for the Timed Monitor Adapter
*
 * @author  Gordon Mckeown <gordon_mckeown@bmc.com>
* @version 1.0
*/
public class TimedMonitorAdapterTest {
    private TimedMonitorAdapterConfig config;
    private TimedMonitorAdapter adapter;
    private AdapterManager am;

    private static final String CONFIG_FILE_ADAPTER = "adapter-config.xml";
    private static final String ADAPTER_ID = "TimedMonitorID";
    private static final String ADAPTER_NAME = "TimedMonitorAdapter";
    private static final String PEER_NAME = "DevStudio";

    @Before
    public void setUp() throws Exception {
        try {
            config = new TimedMonitorAdapterConfig("TestTimedMonitorAdapter");
            XML configXml = loadResourceXml(CONFIG_FILE_ADAPTER);
            config.fromXML(configXml);

            adapter = new TimedMonitorAdapter();
            adapter.setPeerName(PEER_NAME);
            adapter.createConfiguration(ADAPTER_ID);
            adapter.updateConfiguration(config);

            adapter.setName(ADAPTER_NAME);
            am = new MockAdapterManager();
            adapter.initialize(am);

        } catch (InvalidXMLFormatException e) {
            fail("Configuration file XML format error");
        } catch (InvalidConfigurationException e) {
            fail("Invalid entries in configuration file");
        }
    }

    @After
    public void tearDown() throws Exception {
        adapter = null;
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testTimedMonitorAdapter() {
        Thread t = new Thread(adapter, "TestTimedMonitorAdapter");
        t.start();

        try {
            Thread.sleep(20000);
        } catch (Exception e) {
            fail("An exception occurred during the thread sleep.");
        }

        try {
            adapter.shutdown();
            t.join(6000);
        } catch (Exception e) {
            fail("An exception occurred during the thread join");
        } finally {
            assertTrue("Failed to stop adapter!", adapter.getState() == StateEnum.STOPPED);
        }
    }

    /**
     * Method that loads XML document from a resource file
     *
     * @param fileName  File from which to load XML document
     * @return          XML object representing the loaded file
     */
    private XML loadResourceXml(String fileName) {
        XML xmlData = null;
        try {
            InputStream stream = ResourceLocator.getSystemResourceAsInputStream(fileName);
            assertNotNull("Unable to load file resource " + fileName, stream);
            xmlData = XML.read(stream);
        } catch (Exception e) {
            e.printStackTrace();
            fail(e.getMessage());
        }
        return xmlData;
    }
}





 

And the MockAdapterManager class definition (which can live in the same directory as your test class):

 

package com.example.bao.adapter.timedmonitor;

import com.realops.foundation.adapterframework.AdapterEvent;
import com.realops.foundation.adapterframework.AdapterManager;

/**
* A mock adapter manager that simply outputs the event details as a string
*
 * @author      Gordon Mckeown <gordon_mckeown@bmc.com>
* @version     1.0
*
*/
public class MockAdapterManager extends AdapterManager {
    /**
     * Default constructor - performs no action
     */
    public MockAdapterManager() {
        // Do nothing
    }

    /**
     * Handle event sent from monitor adapter
     *
     * @param event AdapterEvent containing event information from adapter
     */

    @Override
    public void sendEvent(AdapterEvent event) {
        System.out.println(event.toXML().toCompactString());
    }


}






 

Finally, the very important adapter configuration XML file (adapter-config.xml) that must live in your test resources folder:

 

<config>
    <grid>
        <grid>
            <adapters>
                <adapter id="TimedMonitorID">
                    <name>TimedMonitorAdapter</name>
                    <class-name>com.example.bao.adapter.timedmonitor.TimedMonitorAdapter</class-name>
                    <config>
                        <sleep-delay>5000</sleep-delay>
                    </config>
                </adapter>
            </adapters>
        </grid>
    </grid>
</config>



 

Supporting Module

Although not essential at this stage of our adapter development, let’s put together a simple workflow to do something with the events being generated. This workflow will just dump the input event into the processes.log file; we could achieve pretty much the same thing by playing with the log levels on our peers (though that would log into grid.log – take a look at "Seeing the Events" below).

 

So create your workflow (I've called mine HandleTimedEvents), set an input parameter of “inputevent” and a single Assign activity that logs the content of inputevent like so:

 

2014-12-03 13_23_20-192.168.0.158 - Remote Desktop Connection.png

 

Ensuring you left the "Expose Process: In Rules" option ticked in your process properties, create a rule within the same module and set it up like this:

 

2014-12-03 13_24_43-192.168.0.158 - Remote Desktop Connection.png

 

Note that Core_Timed_Monitor is the name of my adapter on the grid, so change this to suit if you’re going to call yours something else.

 

Uploading to the Grid

This was detailed in Part 3, so I won’t go through it in full detail again. Just ensure you generate a JAR file, and use the following class name: com.example.bao.adapter.timedmonitor.TimedMonitorAdapter


Use a type of “timed monitor adapter” if your imagination fails you, and don’t forget to upload your supporting module and activate it!

 

Seeing the Events

Increase the log level of the Activity Manager on your peers to DEBUG and you will start to see entries like this one:

 

03 Dec 2014 13:06:44,258 DEBUG DefaultAttributeManager : Thread[AMP - Activity Processor - Process Execution - 3994,5,main] set attribute with name=inputevent, value=<adapter-event><source-adapter>Core_Timed_Monitor</source-adapter><event>Dummy event, loop number: 102</event><data><dummy-event>Core_Timed_Monitor</dummy-event></data></adapter-event>, isGlobal=false, for com.realops.foundation.activityprocessor.executionstate.DefaultAttributeManager@70e7bc6b







 

One quick XML pretty print later and we can see our event in all of its glory:

 

<adapter-event>
    <source-adapter>Core_Timed_Monitor</source-adapter>
    <event>Dummy event, loop number: 102</event>
    <data>
        <dummy-event>Core_Timed_Monitor</dummy-event>
    </data>
</adapter-event>







 

This should help you in defining the rules used to trigger workflows. If your rule is working, you’ll also be seeing the logging entries from your workflow being placed into the processes.log:

 

The process started. It is triggered by a rule.
03 Dec 2014 13:06:44,316 [Current Time=Wed Dec 03 13:06:44 GMT 2014] [Process Name=:BMC_Testing_GMK:SimpleMonitor:HandleTimedEvents] [Root Job Id=5b55545cf05bfc30:-4862ff7a:149a97323ba:-7fc41-1417613209310] [Job Id=5b55545cf05bfc30:-4862ff7a:149a97323ba:-7fc41-1417613209310]
!!!! Timed monitor adapter event received !!!!
[inputevent=
<adapter-event>
  <source-adapter>Core_Timed_Monitor</source-adapter>
  <event>Dummy event, loop number: 102</event>
  <data>
    <dummy-event>Core_Timed_Monitor</dummy-event>
  </data>
</adapter-event>]








What's Next?

At this point you should have this obnoxious little monitor adapter pumping events onto your grid, and a workflow that gets called for each event. In the next part we’ll look at extending the adapter to respond to “real” external events rather than an internal timer.