alma.acs.nc.AcsEventSubscriberImplBase.java Source code

Java tutorial

Introduction

Here is the source code for alma.acs.nc.AcsEventSubscriberImplBase.java

Source

/*
 * ALMA - Atacama Large Millimiter Array
 * (c) European Southern Observatory, 2009 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

package alma.acs.nc;

import static alma.acs.nc.sm.generated.EventSubscriberAction.createConnection;
import static alma.acs.nc.sm.generated.EventSubscriberAction.createEnvironment;
import static alma.acs.nc.sm.generated.EventSubscriberAction.destroyConnection;
import static alma.acs.nc.sm.generated.EventSubscriberAction.destroyEnvironment;
import static alma.acs.nc.sm.generated.EventSubscriberAction.resumeConnection;
import static alma.acs.nc.sm.generated.EventSubscriberAction.suspendConnection;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

import org.apache.commons.scxml.ErrorReporter;
import org.apache.commons.scxml.EventDispatcher;
import org.apache.commons.scxml.SCInstance;
import org.apache.commons.scxml.TriggerEvent;

import alma.ACSErrTypeCommon.wrappers.AcsJBadParameterEx;
import alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx;
import alma.ACSErrTypeCommon.wrappers.AcsJCouldntPerformActionEx;
import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx;
import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx;
import alma.acs.container.ContainerServicesBase;
import alma.acs.exceptions.AcsJException;
import alma.acs.logging.MultipleRepeatGuard;
import alma.acs.logging.RepeatGuard;
import alma.acs.logging.RepeatGuard.Logic;
import alma.acs.nc.sm.generated.EventSubscriberAction;
import alma.acs.nc.sm.generated.EventSubscriberSignal;
import alma.acs.nc.sm.generated.EventSubscriberSignalDispatcher;
import alma.acs.nc.sm.generic.AcsScxmlActionDispatcher;
import alma.acs.nc.sm.generic.AcsScxmlActionExecutor;
import alma.acs.nc.sm.generic.AcsScxmlEngine;
import alma.acs.util.StopWatch;
import alma.acsErrTypeLifeCycle.wrappers.AcsJEventSubscriptionEx;
import alma.acsnc.EventDescription;

/**
 * Base class for an event subscriber, that can be used for both Corba NC and for in-memory test NC.
 * <p>
 * We will have to see if it can also be used for DDS-based events, or if the required modifications will be too heavy.
 * 
 * @param <T> The event (base) type. If all events are of the same type then that type should be used; 
 *            otherwise a common base type for all events that may be sent on the given "channel" should be used, 
 *            such as <code>Object</code> or <code>IDLEntity</code>. 
 */
public abstract class AcsEventSubscriberImplBase<T>
        implements AcsEventSubscriber<T>, AcsScxmlActionExecutor<EventSubscriberAction> {

    /**
     * A name that identifies the client of this NCSubscriber, to be used
     * both for logging and also (if applicable) as the supplier proxy name so that 
     * looking at the proxy objects of the NC we can figure out who the clients are.
     */
    protected final String clientName;

    protected final ContainerServicesBase services;

    /** 
     * Provides access to the ACS logging system. 
     */
    protected final Logger logger;

    /**
     * State machine for the subscriber lifecycle. 
     * It is not used for data handling itself, even though the measured overhead 
     * of less than 0.2 ms per signal might allow this.
     */
    protected final AcsScxmlEngine<EventSubscriberSignal, EventSubscriberAction> stateMachine;

    private static final String scxmlFileName = "/alma/acs/nc/sm/generated/EventSubscriberSCXML.xml";

    protected final EventSubscriberSignalDispatcher stateMachineSignalDispatcher;

    /**
     * Event queue should hold at least two events to avoid unnecessary scary logs about slow receivers,
     * but must be short enough to get receivers to actually implement their own queue and discard mechanism
     * instead of relying on this ACS queue which may buffer events for a limited time and thus obscure the problem.
     * @see #eventHandlingExecutor
     */
    public static final int EVENT_QUEUE_CAPACITY = 50;

    /**
     * Single-thread executor with a small queue (size given in {@link #EVENT_QUEUE_CAPACITY}, 
     * used to stay responsive toward the NC even if receivers are slow. 
     * The purpose is only to track down receivers that don't keep up with the event rate, and not to provide reliable event buffering, 
     * see http://jira.alma.cl/browse/COMP-5767.
     * <p>
     * Errors are logged, using also fields {@link #numEventsDiscarded} and {@link #receiverTooSlowLogRepeatGuard}.
     * <p>
     * The difference of this mechanism compared to the logs controlled by {@link #processTimeLogRepeatGuard} is that
     * here we take into account the actual event rate and check whether the receiver can handle it, 
     * whereas {@link #processTimeLogRepeatGuard} only compares the actual process times against pre-configured values.
     */
    private ThreadPoolExecutor eventHandlingExecutor;

    /**
     * @see #eventHandlingExecutor
     */
    private final AtomicLong numEventsDiscarded = new AtomicLong(0);

    /**
     * Throttles logs from {@link #logEventProcessingTooSlowForEventRate(long, String, long)}.
     */
    private final RepeatGuard receiverTooSlowLogRepeatGuard;

    /**
     * Contains a generic receiver to be used by the
     * {@link #addGenericSubscription()} method.
     */
    protected AcsEventSubscriber.GenericCallback genericReceiver;

    /**
     * Contains a list of receiver functions to be invoked when an event 
     * of a particular type is received.
     * <p>
     * key = the event type (Java class derived from IDL struct). <br>
     * value = the matching event handler.
     */
    protected final Map<Class<? extends T>, Callback<? extends T>> receivers = new HashMap<Class<? extends T>, Callback<? extends T>>();

    /**
     * Contains a list of repeat guards for each different type of event.
     */
    protected final MultipleRepeatGuard processTimeLogRepeatGuard;

    /**
     * Runtime access to the type parameter &lt;T&gt;.
     */
    protected final Class<T> eventType;

    /**
     * @return <code>true</code> if events should be logged when they are received. 
     */
    protected abstract boolean isTraceEventsEnabled();

    /**
     * Base class constructor, to be called from subclass ctor.
     * IMPORTANT: Subclasses MUST call "stateMachine.setUpEnvironment()" at the end of their constructors.
     * This is because Java does not support template method design for constructors 
     * (see for example http://stackoverflow.com/questions/2906958/running-a-method-after-the-constructor-of-any-derived-class).
     * Since the subclasses are all meant to be produced within ACS I did not want to go for dirty tricks to work around this.
     * We also don't want to call a public init() method after constructing the subscriber. 
     * <p>
     * Normally an ACS class such as container services will act as the factory for event subscriber objects, 
     * but for exceptional cases it is also possible to create one stand-alone, 
     * as long as the required parameters can be provided.
     * 
     * @param services
     *            To get ACS logger, access to the CDB, etc.
     * @param clientName
     *            A name that identifies the client of this NCSubscriber. 
     *            TODO: Check if we still need this name to be specified separately from {@link services#getName()}.
     * @throws AcsJException
     *             Thrown on any <I>really bad</I> error conditions encountered.
     */
    public AcsEventSubscriberImplBase(ContainerServicesBase services, String clientName, Class<T> eventType)
            throws AcsJException {

        if (services == null) {
            AcsJBadParameterEx ex = new AcsJBadParameterEx();
            ex.setParameter("services");
            ex.setParameterValue("null");
            throw ex;
        }
        this.services = services;

        if (clientName == null) {
            AcsJBadParameterEx ex = new AcsJBadParameterEx();
            ex.setParameter("clientName");
            ex.setParameterValue("null");
            throw ex;
        }
        this.clientName = clientName;

        this.eventType = eventType;
        logger = services.getLogger();

        // @TODO Set more realistic guarding parameters, e.g. max 1 identical log in 10 seconds
        processTimeLogRepeatGuard = new MultipleRepeatGuard(0, TimeUnit.SECONDS, 1, Logic.COUNTER, 100);

        // log slow receiver error only every 30 seconds or every 100 times, whatever comes first
        receiverTooSlowLogRepeatGuard = new RepeatGuard(30, TimeUnit.SECONDS, 100, Logic.OR);

        // set up the state machine

        AcsScxmlActionDispatcher<EventSubscriberAction> actionDispatcher = new AcsScxmlActionDispatcher<EventSubscriberAction>(
                logger, EventSubscriberAction.class);

        // As a design choice, we implement all SM actions directly here in this class and its subclasses.
        // The state machine framework expects an object per action, so that we must register ourselves for every action type.
        // We could do this as a for loop over EventSubscriberAction.values(), but better hardcode the registration
        // for every action type so that SM changes will be noticed more easily.
        actionDispatcher.registerActionHandler(createEnvironment, this);
        actionDispatcher.registerActionHandler(destroyEnvironment, this);
        actionDispatcher.registerActionHandler(createConnection, this);
        actionDispatcher.registerActionHandler(destroyConnection, this);
        actionDispatcher.registerActionHandler(suspendConnection, this);
        actionDispatcher.registerActionHandler(resumeConnection, this);

        // Here the AcsScxmlEngine constructor will load the scxml file and start the state machine
        stateMachine = new AcsScxmlEngine<EventSubscriberSignal, EventSubscriberAction>(scxmlFileName, logger,
                actionDispatcher, EventSubscriberSignal.class);

        // Convenience method to send events to the SM. 
        // Usually meant to be used as a base class, but here we don't want to expose the SM to the user.
        stateMachineSignalDispatcher = new EventSubscriberSignalDispatcher() {
            @Override
            protected AcsScxmlEngine<EventSubscriberSignal, EventSubscriberAction> getScxmlEngine() {
                return stateMachine;
            }
        };

        // Later at the end of the subclass ctor there should be the call to 
        // stateMachine.setUpEnvironment(), leaving our state machine in state EnvironmentCreated
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////// State machine //////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Dispatches the action enum to one of the action handler methods.
     * <p>
     * The overhead of implementing this method is the price we pay for avoiding reflection
     * and allowing flexible action implementation in the SM design.
     * @see alma.acs.nc.sm.generic.AcsScxmlActionExecutor#execute(java.lang.Enum, org.apache.commons.scxml.EventDispatcher, org.apache.commons.scxml.ErrorReporter, org.apache.commons.scxml.SCInstance, java.util.Collection)
     */
    @Override
    public boolean execute(EventSubscriberAction action, EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        //      logger.fine("Will handle action " + action.name());
        boolean ret = true;

        switch (action) {

        case createEnvironment:
            createEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        case destroyEnvironment:
            destroyEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        case createConnection:
            createConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        case destroyConnection:
            destroyConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        case suspendConnection:
            suspendAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        case resumeConnection:
            resumeAction(evtDispatcher, errRep, scInstance, derivedEvents);
            break;

        default:
            ret = false;
        }

        //      logger.fine("Done handling action " + action.name());
        return ret;
    }

    /**
     * Subclass may override, but must call super.createEnvironmentAction().
     */
    protected void createEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {
        // nada
    }

    /**
     * Subclass may override, but must call super.destroyEnvironmentAction().
     */
    protected void destroyEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {
    }

    /**
     * Subclass may override, but must call super.createConnectionAction().
     */
    protected void createConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {
        eventHandlingExecutor = new ThreadPoolExecutor(0, 1, 1L, TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(EVENT_QUEUE_CAPACITY), services.getThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }

    /**
     * Handler for "destroyConnection" state machine action.
     * <p>
     * Shuts down the event queue. 
     * Queued events may still be processed by the receivers afterwards, 
     * but here we wait for up to 500 ms to log it if it is the case.
     * <p>
     * Further events delivered to {@link #processEventAsync(Object, EventDescription)}
     * will cause an exception there.
     * <p>
     * Subclass may override, but must call super.destroyConnectionAction().
     */
    protected void destroyConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        eventHandlingExecutor.shutdown();
        boolean queueOK = false; // just in case we want to report the success of this shutdown
        try {
            queueOK = eventHandlingExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            // just leave queueOK == false
        }
        if (!queueOK) {
            // interrupted or timeout occurred, may still have events in the queue. Terminate with error message
            int remainingEvents = eventHandlingExecutor.getQueue().size();
            logQueueShutdownError(500, remainingEvents);
        }
    }

    /**
     * Subclass may override, but must call super.suspendAction().
     */
    protected void suspendAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance,
            Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {
        // nada
    }

    /**
     * Subclass may override, but must call super.resumeAction().
     */
    protected void resumeAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance,
            Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {
        // nada
    }

    /**
     * Gets the configured (or default) max time that a receiver may take to process an event,
     * regardless of the actual event rate.
     */
    protected abstract double getMaxProcessTimeSeconds(String eventName);

    /**
     * Logs an exception thrown by an event handler (user code).
     */
    protected abstract void logEventReceiveHandlerException(String eventName, String receiverClassName,
            Throwable thr);

    /**
     * Logs the error that event processing time was exceeded.
     */
    protected abstract void logEventProcessingTimeExceeded(String eventName, long logOcurrencesNumber);

    /**
     * Logs the error that the receiver cannot keep up with the actual event rate,
     * in spite of the small event buffering done.
     * 
     * @param numEventsDiscarded   The number of events that have actually been discarded since the last log. 
     *                             Will often be 0 when we just warn about the queue filling up.
     * @param eventName
     */
    protected abstract void logEventProcessingTooSlowForEventRate(long numEventsDiscarded, String eventName);

    /**
     * Logs or ignores the fact that an event was received for which no receiver could be found.
     * <p>
     * The subclass must know whether such a condition is expected or not, 
     * e.g. because event filtering is set up outside of the subscriber
     * and only subscribed event types are expected to arrive. 
     */
    protected abstract void logNoEventReceiver(String eventName);

    /**
     * Logs the error that the local event buffer could not be emptied before shutting down the subscriber. 
     */
    protected abstract void logQueueShutdownError(int timeoutMillis, int remainingEvents);

    /**
     * Asynchronously calls {@link #processEvent(Object, EventDescription)}, 
     * using {@link #eventHandlingExecutor}.
     * <p>
     * This method should be called from the subclass-specific method that receives the event,
     * for example <code>push_structured_event</code> in case of Corba NC.
     * <p>
     * This method is thread-safe.
     * 
     * @param eventData (defined as <code>Object</code> instead of <code>T</code> to include data for generic subscription).
     * @param eventDesc event meta data
     */
    protected void processEventAsync(final Object eventData, final EventDescription eventDesc) {

        // to avoid unnecessary scary logs, we tolerate previous events up to half the queue size
        boolean isReceiverBusyWithPreviousEvent = (eventHandlingExecutor.getQueue().size() > EVENT_QUEUE_CAPACITY
                / 2);

        //         logger.info("Queue size: " + eventHandlingExecutor.getQueue().size());

        boolean thisEventDiscarded = false;
        try {
            eventHandlingExecutor.execute(new Runnable() {
                public void run() {
                    // here we call processEvent from the worker thread
                    processEvent(eventData, eventDesc);
                }
            });
        } catch (RejectedExecutionException ex) {
            // receivers have been too slow, queue is actually full, will drop data.
            thisEventDiscarded = true;
            numEventsDiscarded.incrementAndGet();
        }
        if (thisEventDiscarded || isReceiverBusyWithPreviousEvent) {
            // receiverTooSlowLogRepeatGuard currently not thread-safe, therefore synchronize
            synchronized (receiverTooSlowLogRepeatGuard) {
                if (receiverTooSlowLogRepeatGuard.checkAndIncrement()) {
                    // About numEventsDiscarded and concurrency: 
                    // That counter may have been incremented by other threads between the above RejectedExecutionException
                    // and here. These threads are blocked now, and have not yet incremented the repeat guard.
                    // This can lead to some harmless irregularities in how often we actually log the message.
                    // What matters is that we report correctly the number of discarded events. 
                    logEventProcessingTooSlowForEventRate(numEventsDiscarded.getAndSet(0),
                            eventData.getClass().getName());
                }
            }
        }
    }

    /**
     * This method should be called from the subclass-specific method that receives the event,
     * for example <code>push_structured_event</code> in case of Corba NC,
     * or preferably via {@link #processEventAsync(Object, EventDescription)}.
     * <p>
     * No exception is allowed to be thrown by this method, even if the receiver implementation throws a RuntimeExecption
     * @param eventData (defined as <code>Object</code> instead of <code>T</code> to include data for generic subscription).
     * @param eventDesc
     */
    protected void processEvent(Object eventData, EventDescription eventDesc) {

        Class<?> incomingEventType = eventData.getClass();
        String eventName = incomingEventType.getName();

        // figure out how much time this event has to be processed (according to configuration)
        double maxProcessTimeSeconds = getMaxProcessTimeSeconds(eventName);

        StopWatch profiler = new StopWatch();

        // we give preference to a receiver that has registered for this event type T or a subtype
        if (eventType.isAssignableFrom(incomingEventType) && receivers.containsKey(incomingEventType)) {

            @SuppressWarnings("unchecked")
            T typedEventData = (T) eventData;

            Callback<? extends T> receiver = receivers.get(incomingEventType);

            profiler.reset();
            try {
                _process(receiver, typedEventData, eventDesc);
            } catch (Throwable thr) {
                logEventReceiveHandlerException(eventName, receiver.getClass().getName(), thr);
            }
            double usedSecondsToProcess = (profiler.getLapTimeMillis() / 1000.0);

            // warn the end-user if the receiver is taking too long, using a repeat guard
            if (usedSecondsToProcess > maxProcessTimeSeconds
                    && processTimeLogRepeatGuard.checkAndIncrement(eventName)) {
                logEventProcessingTimeExceeded(eventName,
                        processTimeLogRepeatGuard.counterAtLastExecution(eventName));
            }
        }
        // fallback to generic receive method
        else if (genericReceiver != null) {

            profiler.reset();
            genericReceiver.receiveGeneric(eventData, eventDesc);
            double usedSecondsToProcess = (profiler.getLapTimeMillis() / 1000.0);

            // warn the end-user if the receiver is taking too long 
            if (usedSecondsToProcess > maxProcessTimeSeconds
                    && processTimeLogRepeatGuard.checkAndIncrement(eventName)) {
                logEventProcessingTimeExceeded(eventName,
                        processTimeLogRepeatGuard.counterAtLastExecution(eventName));
            }
        }
        // No receiver found.
        // This may be OK or not, depending on whether the subclass sets up filtering in the underlying notification framework
        // that ensures that only subscribed event types reach the subscriber. Subclass should decide if and how to report this.
        else {
            logNoEventReceiver(eventName);
        }
    }

    /**
     * "Generic helper method" to enforce type argument inference by the compiler,
     * see http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ207
     */
    private <U extends T> void _process(Callback<U> receiver, T eventData, EventDescription eventDescrip) {
        U castCorbaData = null;
        try {
            castCorbaData = receiver.getEventType().cast(eventData);
        } catch (ClassCastException ex) {
            // This should never happen and would be an ACS error
            logger.warning("Failed to deliver incompatible data '" + eventData.getClass().getName()
                    + "' to subscriber '" + receiver.getEventType().getName()
                    + "'. Fix data subscription handling in " + getClass().getName() + "!");
        }
        // user code errors (runtime ex etc) we let fly up
        receiver.receive(castCorbaData, eventDescrip);
    }

    @Override
    public String getLifecycleState() {
        return stateMachine.getCurrentState();
    }

    /**
     * Use only for unit testing!
     */
    public boolean hasGenericReceiver() {
        return (genericReceiver != null);
    }

    /**
     * Use only for unit testing!
     */
    public int getNumberOfReceivers() {
        return receivers.size();
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////// AcsEventSubscriber impl //////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Subscribes to all events. The latest generic receiver displaces the previous one. 
     * <p>
     * If in addition to this generic subscription we also have specific subscriptions via
     * {@link #addSubscription(Class, Callback)},
     * then those more specific subscriptions will take precedence in receiving an event.
     * <p>
     * Notice though that any server-side filters previously created for the event type specific 
     * subscriptions get deleted when calling this method, so that even after removing a generic 
     * subscription the network performance gain of server-side filtering is lost. 
     * @TODO: Couldn't this be fixed by creating the server-side filters on demand (see also javadoc class comment about lifecycle)?
     */
    @Override
    public final void addGenericSubscription(GenericCallback receiver) throws AcsJEventSubscriptionEx {

        // First time we create the filter and set the receiver
        if (genericReceiver == null) {
            notifyFirstSubscription(null);
        }
        // After the filter is created, we just replace the receiver
        genericReceiver = receiver;
    }

    /**
     * Removes the generic receiver handler.
     * 
     * @throws AcsJCORBAProblemEx
     */
    @Override
    public final void removeGenericSubscription() throws AcsJEventSubscriptionEx {

        if (genericReceiver == null) {
            AcsJEventSubscriptionEx ex = new AcsJEventSubscriptionEx();
            ex.setContext("Failed to remove generic subscription when not actually subscribed.");
            ex.setEventType("generic");
            throw ex;
        }

        notifySubscriptionRemoved(null);
        genericReceiver = null;
    }

    @Override
    public final <U extends T> void addSubscription(Callback<U> receiver) throws AcsJEventSubscriptionEx {

        Class<U> subscribedEventType = receiver.getEventType();

        if (subscribedEventType == null || !(eventType.isAssignableFrom(subscribedEventType))) {
            AcsJEventSubscriptionEx ex = new AcsJEventSubscriptionEx();
            ex.setContext("Receiver is returning a null or invalid event type. "
                    + "Check the getEventType() method implementation and try again.");
            ex.setEventType(subscribedEventType == null ? "null" : subscribedEventType.getName());
            throw ex;
        }

        // First time we create the filter and set the receiver
        if (!receivers.containsKey(subscribedEventType)) {
            notifyFirstSubscription(subscribedEventType);
        }
        // After the filter is created, we just replace the corresponding receivers
        receivers.put(subscribedEventType, receiver);
    }

    @Override
    public final <U extends T> void removeSubscription(Class<U> structClass) throws AcsJEventSubscriptionEx {

        // Removing subscription from receivers list
        if (structClass != null) {

            if (receivers.containsKey(structClass)) {
                receivers.remove(structClass);
                notifySubscriptionRemoved(structClass);
            } else {
                AcsJEventSubscriptionEx ex = new AcsJEventSubscriptionEx();
                ex.setContext("Trying to unsubscribe from an event type not being subscribed to.");
                ex.setEventType(structClass.getName());
                throw ex;
            }
        } else {
            // Removing every type of event
            receivers.clear();
            notifyNoSubscription();
        }
    }

    @Override
    public final void startReceivingEvents() throws AcsJIllegalStateEventEx, AcsJCouldntPerformActionEx {
        try {
            stateMachineSignalDispatcher.startReceivingEvents();
        } catch (AcsJStateMachineActionEx ex) {
            throw new AcsJCouldntPerformActionEx(ex);
        }
    }

    /** 
     * @see alma.acs.nc.AcsEventSubscriber#disconnect()
     */
    @Override
    public final void disconnect() throws AcsJIllegalStateEventEx, AcsJCouldntPerformActionEx {
        try {
            stateMachineSignalDispatcher.stopReceivingEvents();
        } catch (AcsJIllegalStateEventEx ex) {
            // ignore. If the state is totally wrong then the subsequent cleanUpEnvironment
            // will also throw AcsJIllegalStateEventEx
        } catch (AcsJStateMachineActionEx ex) {
            throw new AcsJCouldntPerformActionEx(ex);
        } finally {
            // even after AcsJIllegalStateEventEx in stopReceivingEvents we want to do the second clean-up step
            try {
                stateMachineSignalDispatcher.cleanUpEnvironment();
            } catch (AcsJStateMachineActionEx ex) {
                throw new AcsJCouldntPerformActionEx(ex);
            }
        }
    }

    @Override
    public final void suspend() throws AcsJIllegalStateEventEx, AcsJCouldntPerformActionEx {
        try {
            stateMachineSignalDispatcher.suspend();
        } catch (AcsJStateMachineActionEx ex) {
            throw new AcsJCouldntPerformActionEx(ex);
        }
    }

    @Override
    public final void resume() throws AcsJIllegalStateEventEx, AcsJCouldntPerformActionEx {
        try {
            stateMachineSignalDispatcher.resume();
        } catch (AcsJStateMachineActionEx ex) {
            throw new AcsJCouldntPerformActionEx(ex);
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////// Subscription helper methods //////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * @param structClass Can be <code>null</code> in case of generic subscription.
     */
    protected abstract void notifyFirstSubscription(Class<?> structClass) throws AcsJEventSubscriptionEx;

    /**
     * @param structClass
     * @throws AcsJEventSubscriptionEx
     */
    protected abstract void notifySubscriptionRemoved(Class<?> structClass) throws AcsJEventSubscriptionEx;

    /**
     * 
     */
    protected abstract void notifyNoSubscription();

    @Override
    public boolean isSuspended() {
        return stateMachine.isStateActive("EnvironmentCreated::Connected::Suspended");
    }

    public final boolean isDisconnected() {
        return (stateMachine.isStateActive("EnvironmentCreated::Disconnected")
                || stateMachine.isStateActive("EnvironmentUnknown"));
    }

}