/* * 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 java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; 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 org.omg.CORBA.BAD_PARAM; import org.omg.CORBA.IntHolder; import org.omg.CORBA.NO_IMPLEMENT; import org.omg.CORBA.portable.IDLEntity; import org.omg.CosEventComm.Disconnected; import org.omg.CosNaming.NamingContext; import org.omg.CosNotification.EventType; import org.omg.CosNotification.StructuredEvent; import org.omg.CosNotification.UnsupportedAdmin; import org.omg.CosNotification.UnsupportedQoS; import org.omg.CosNotifyChannelAdmin.AdminLimitExceeded; import org.omg.CosNotifyChannelAdmin.AdminNotFound; import org.omg.CosNotifyChannelAdmin.ClientType; import org.omg.CosNotifyChannelAdmin.InterFilterGroupOperator; import org.omg.CosNotifyChannelAdmin.ProxyNotFound; import org.omg.CosNotifyChannelAdmin.ProxySupplier; import org.omg.CosNotifyChannelAdmin.ProxyType; import org.omg.CosNotifyChannelAdmin.StructuredProxyPushSupplier; import org.omg.CosNotifyChannelAdmin.StructuredProxyPushSupplierHelper; import org.omg.CosNotifyComm.InvalidEventType; import org.omg.CosNotifyFilter.ConstraintExp; import org.omg.CosNotifyFilter.Filter; import org.omg.CosNotifyFilter.FilterFactory; import org.omg.CosNotifyFilter.FilterNotFound; import gov.sandia.NotifyMonitoringExt.ConsumerAdmin; import gov.sandia.NotifyMonitoringExt.ConsumerAdminHelper; import gov.sandia.NotifyMonitoringExt.EventChannel; import gov.sandia.NotifyMonitoringExt.EventChannelFactory; import gov.sandia.NotifyMonitoringExt.NameAlreadyUsed; import gov.sandia.NotifyMonitoringExt.NameMapError; import alma.ACSErrTypeCORBA.wrappers.AcsJNarrowFailedEx; import alma.ACSErrTypeCommon.wrappers.AcsJBadParameterEx; import alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx; import alma.ACSErrTypeCommon.wrappers.AcsJIllegalArgumentEx; import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx; import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx; import alma.AcsNCTraceLog.LOG_NC_ConsumerAdminObtained_OK; import alma.AcsNCTraceLog.LOG_NC_ConsumerAdmin_Overloaded; import alma.AcsNCTraceLog.LOG_NC_EventReceive_FAIL; import alma.AcsNCTraceLog.LOG_NC_EventReceive_HandlerException; import alma.AcsNCTraceLog.LOG_NC_EventReceive_NoHandler; import alma.AcsNCTraceLog.LOG_NC_EventReceive_OK; import alma.AcsNCTraceLog.LOG_NC_ProcessingTimeExceeded; import alma.AcsNCTraceLog.LOG_NC_ReceiverTooSlow; import alma.AcsNCTraceLog.LOG_NC_SubscriptionConnect_FAIL; import alma.AcsNCTraceLog.LOG_NC_SubscriptionConnect_OK; import alma.AcsNCTraceLog.LOG_NC_SupplierProxyCreation_FAIL; import alma.AcsNCTraceLog.LOG_NC_SupplierProxyCreation_OK; import alma.AcsNCTraceLog.LOG_NC_TaoExtensionsSubtypeMissing; import alma.JavaContainerError.wrappers.AcsJContainerServicesEx; import alma.acs.container.ContainerServicesBase; import alma.acs.exceptions.AcsJException; import alma.acs.logging.AcsLogLevel; import alma.acs.ncconfig.EventDescriptor; import alma.acsErrTypeLifeCycle.wrappers.AcsJEventSubscriptionEx; import alma.acsnc.EventDescription; import alma.acsnc.EventDescriptionHelper; import alma.acsnc.OSPushConsumer; import alma.acsnc.OSPushConsumerHelper; import alma.acsnc.OSPushConsumerOperations; import alma.acsnc.OSPushConsumerPOA; import alma.acsnc.OSPushConsumerPOATie; /** * NCSubscriber is the Java implementation of the Notification Channel subscriber, * while following the more generic {@link AcsEventSubscriber} interface. * <p> * This class is used to receive events asynchronously from notification channel suppliers. * It is the replacement of {@link alma.acs.nc.Consumer}, and to keep things simple it no longer * supports the inheritance mode, but instead supports type-safe delegation of incoming calls to * a user-supplied handler. * <p> * The lifecycle steps are: * <ul> * <li>During creation of an NCSubscriber, the NC and consumer admin objects are either created or reused, * and a proxy supplier object is created, all inside the notify service. <br> * The reason for creating the proxy supplier (and the other objects along) already at this stage is * to support event filtering on the server side, with {@link Filter} objects getting attached to the * proxy supplier, see {@link #addSubscription(alma.acs.nc.AcsEventSubscriber.Callback)}. <br> * TODO: This implementation could be changed to create the server-side filters on demand (e.g. in startReceivingEvents), * so that addSubscription only stores the event type information without yet creating the filter. * <li>Handlers for specialized events ({@link #addSubscription(alma.acs.nc.AcsEventSubscriber.Callback)}) * and/or for all events ({@link #addGenericSubscription(alma.acs.nc.AcsEventSubscriber.GenericCallback)}) can be registered. * <li>Once {@link #startReceivingEvents()} is called, Corba NCs push events to this class, which delegates * the events to the registered handlers. * <li>The connection can then be suspended or resumed. * </ul> * The NCSubscriber gets created (and cleaned up if needed) through the container services. * Note about refactoring: NCSubscriber gets instantiated in module jcont using java reflection. * Thus if you change the package, name, or constructor of this class, make sure to fix the corresponding "forName" call in jcont. * * @param <T> See base class. * * @author jslopez, hsommer, rtobar */ public class NCSubscriber<T extends IDLEntity> extends AcsEventSubscriberImplBase<T> implements OSPushConsumerOperations, ReconnectableParticipant { /** * The default maximum amount of time an event handler is given to process * event before a warning message is logged. The time unit is floating point seconds. * Here we cache the default value defined in EventChannel.xsd, using an XSD binding class. */ private static final double DEFAULT_MAX_PROCESS_TIME_SECONDS = (new EventDescriptor()).getMaxProcessTime(); /** Provides access to the notify service and CDB, creates NCs, etc */ protected final Helper helper; /** * There can be only one notification channel for any given subscriber. * The NC is created on demand. * Already in the constructor of this class, the NC's admin object and proxy supplier objects are created or reused. */ protected EventChannel channel; /** The channel has exactly one name registered in the CORBA Naming Service. */ protected final String channelName; /** The channel notification service domain name, can be <code>null</code>. */ protected final String channelNotifyServiceDomainName; /** * The consumer admin object attached to the NC, * which is used by subscribers to get a reference to the structured supplier proxy. * This reference is <code>null</code> when the subscriber is not connected to a NC. * <p> * The TAO extensions would allow us to set a meaningful name for the admin object, but it * still does not get used as the ID, but as a separate name field. * You can get the consumer admin object ID from here, see {@link ConsumerAdmin#MyID()}. * (In the NC spec, it says "The MyID attribute is a readonly attribute that maintains * the unique identifier of the target ConsumerAdmin instance, which is assigned to it * upon creation by the Notification Service event channel.) It is an integer type, which makes * it necessarily different from the name used with the TAO extensions. * <p> * We try to reuse an admin object for a limited number of subscribers, * to not allocate a new thread in the notification service for every subscriber * but instead get a flexible thread::subscriber mapping. * * @see #PROXIES_PER_ADMIN */ protected ConsumerAdmin sharedConsumerAdmin; /** * Maximum number of proxies (subscribers) per admin object. * @see #sharedConsumerAdmin */ protected static final int PROXIES_PER_ADMIN = 5; /** * The supplier proxy we are connected to. */ protected StructuredProxyPushSupplier proxySupplier; /** * The tie poa wrapped around this object, so that we can receive event data over corba. */ private OSPushConsumerPOA corbaObj; /** * Like {@link #corbaObj}, but activated. */ private OSPushConsumer corbaRef; /** * Helper class used to manipulate CORBA anys. */ protected AnyAide anyAide; /** * Whether receiving events should be logged. */ private final boolean isTraceNCEventsEnabled; /** * Maps event names to the maximum amount of time allowed for receiver * methods to complete. Time is given in floating point seconds. */ protected final HashMap<String, Double> handlerTimeoutMap; /** * Contains a list of the added and removed subscription filters applied. * Key = Event type name (Class#getSimpleName()) * Value = Filter ID (assigned by the NC) */ protected final Map<String, Integer> subscriptionsFilters = new HashMap<String, Integer>(); /** * Supports reconnection after service restart, see TAO's "topology persistence" extension. * @see #reconnect(EventChannelFactory) * @see NotifyExt.ReconnectionCallbackOperations */ private AcsNcReconnectionCallback channelReconnectionCallback; /** * We log it only once if {@link #push_structured_event_called(StructuredEvent)} * vetoes down the regular event processing by this NCSubscriber. */ private boolean firstSubclassVeto = true; /** * To be used only for unit tests. */ private volatile NoEventReceiverListener noEventReceiverListener; /** * Creates a new instance of NCSubscriber. * Normally an ACS class such as container services will act as the factory for NCSubscriber objects, * but for exceptional cases it is also possible to create one stand-alone, * as long as the required parameters can be provided. * * @param channelName * Subscribe to events on this channel registered in the CORBA * Naming Service. If the channel does not exist, it's * registered. * @param channelNotifyServiceDomainName * Channel domain name, which is being used to determine the * notification service that should host the NC. * Passing <code>null</code> results in the default notify service "NotifyEventChannelFactory" being used. * @param services * To get ACS logger, access to the CDB, etc. * @param namingService * Must be passed explicitly, instead of the old hidden approach via <code>ORBInitRef.NameService</code> property. * @param clientName * @param eventType Our type parameter, either <code>IDLEntity</code> as base type or a concrete IDL-defined struct. * @throws AcsJException * Thrown on any <I>really bad</I> error conditions encountered. */ public NCSubscriber(String channelName, String channelNotifyServiceDomainName, ContainerServicesBase services, NamingContext namingService, String clientName, Class<T> eventType) throws AcsJException { super(services, clientName, eventType); // This class will be instantiated through reflection, with an ugly cast, // so that in spite of the declaration "NCSubscriber<T extends IDLEntity>" we must verify that eventType is an IDLEntity. if (!IDLEntity.class.isAssignableFrom(eventType)) { AcsJBadParameterEx ex = new AcsJBadParameterEx(); ex.setParameter("eventType"); ex.setParameterValue(eventType.getName()); ex.setReason("For NCSubscriber, 'eventType' must be (a subtype of) IDLEntity."); throw ex; } if (channelName == null) { AcsJBadParameterEx ex = new AcsJBadParameterEx(); ex.setParameter("channelName"); ex.setParameterValue("null"); throw ex; } if (namingService == null) { AcsJBadParameterEx ex = new AcsJBadParameterEx(); ex.setParameter("namingService"); ex.setParameterValue("null"); throw ex; } this.channelName = channelName; this.channelNotifyServiceDomainName = channelNotifyServiceDomainName; anyAide = new AnyAide(services); helper = new Helper(channelName, channelNotifyServiceDomainName, services, namingService); // populate the map with the maxProcessTime an event receiver processing should take handlerTimeoutMap = helper.getEventHandlerTimeoutMap(); isTraceNCEventsEnabled = helper.getChannelProperties().isTraceEventsEnabled(this.channelName); // this call is mandatory, see base class ctor comment. // It will lead to a call to 'EnvironmentActionHandler#create', // see 'createEnvironmentAction' below. stateMachineSignalDispatcher.setUpEnvironment(); } //////////////////////////////////////////////////////////////////////////////////////// /////////////////////// State machine actions ////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// protected void createEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { super.createEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents); try { // get the channel channel = helper.getNotificationChannel(getNotificationFactoryName()); // get the admin object // Note that admin creation and proxy supplier creation are not synchronized across subscribers, // which means that concurrent creation of subscribers can lead to race conditions // where we end up with too many (> PROXIES_PER_ADMIN) subscribers // for the same admin object. // It would be easy to put a static lock around these two calls, which would take care of // concurrent subscribers from the same component or client. Still there would be the same // racing issues coming from distributed subscribers. // We prefer to not even do local synchronization because then even in simple unit tests // from a single process we can verify the concurrency behavior of subscribers and notifyService. // TODO: Revisit the "synchronized(NCSubscriber.class)" block we currently have inside getSharedAdmin(), // which is giving partial local synchronization, leading to fewer race conditions. // Probably should be removed, or pulled up here and extended around createProxySupplier. sharedConsumerAdmin = getSharedAdmin(); // get the proxy Supplier proxySupplier = createProxySupplier(); // Just check if our shared consumer admin is handling more proxies than it should, and log it // (11) goes for the dummy proxy that we're using the transition between old and new NC classes int currentProxies = sharedConsumerAdmin.push_suppliers().length - 1; if( currentProxies > PROXIES_PER_ADMIN ) { LOG_NC_ConsumerAdmin_Overloaded.log(logger, sharedConsumerAdmin.MyID(), currentProxies, PROXIES_PER_ADMIN, channelName, channelNotifyServiceDomainName == null ? "none" : channelNotifyServiceDomainName); } // The user might create this object, and later call startReceivingEvents(), without attaching any receiver. // If so, it's useless to get all the events, so we start with an all-exclusive filter in the server discardAllEvents(); // } catch (OBJECT_NOT_EXIST ex) { // TODO handle dangling NC binding in the naming service (after notify service restart) } catch (Throwable thr) { throw new AcsJStateMachineActionEx(thr); } } protected void destroyEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { try { if (proxySupplier != null) { // spec 3.3.10.1: "The disconnect_structured_push_supplier operation is invoked to terminate a // connection between the target StructuredPushSupplier and its associated consumer. // This operation takes no input parameters and returns no values. The result of this // operation is that the target StructuredPushSupplier will release all resources it had // allocated to support the connection, and dispose its own object reference." proxySupplier.disconnect_structured_push_supplier(); proxySupplier = null; logger.finer("Disconnected and destroyed the supplier proxy"); } } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex1) { // This is unexpected but OK, because someone else has already destroyed the remote resources logger.fine("No need to release resources for channel " + channelName + " because the NC has been destroyed already."); } finally { // TODO: Should we not try to destroy an empty consumer admin object? // Or is it too risky because of possible race conditions with newly created other subscribers, // given that we don't have a clear distributed locking strategy? sharedConsumerAdmin = null; channel = null; } super.destroyEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents); } protected void createConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { super.createConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents); try { // Register callback for subscribed events if (corbaRef == null) { corbaObj = new OSPushConsumerPOATie(NCSubscriber.this); corbaRef = OSPushConsumerHelper.narrow(helper.getContainerServices().activateOffShoot(corbaObj)); } // Register callback for reconnection requests channelReconnectionCallback = new AcsNcReconnectionCallback(NCSubscriber.this, logger); channelReconnectionCallback.registerForReconnect(services, helper.getNotifyFactory()); // if the factory is null, the reconnection callback is not registered proxySupplier.connect_structured_push_consumer(org.omg.CosNotifyComm.StructuredPushConsumerHelper.narrow(corbaRef)); } catch (AcsJContainerServicesEx e) { LOG_NC_SubscriptionConnect_FAIL.log(logger, channelName, getNotificationFactoryName()); throw new AcsJStateMachineActionEx(e); } catch (org.omg.CosEventChannelAdmin.AlreadyConnected e) { throw new AcsJStateMachineActionEx(new AcsJIllegalStateEventEx(e)); } catch (org.omg.CosEventChannelAdmin.TypeError ex) { LOG_NC_SubscriptionConnect_FAIL.log(logger, channelName, getNotificationFactoryName()); throw new AcsJStateMachineActionEx(ex); } catch (AcsJIllegalArgumentEx ex) { throw new AcsJStateMachineActionEx(ex); } LOG_NC_SubscriptionConnect_OK.log(logger, channelName, getNotificationFactoryName()); } protected void destroyConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // TODO: CHeck if we need to suspend first (was like that in older impl) // try { // suspendAction(evtDispatcher, errRep, scInstance, derivedEvents); // } catch () { // // } /* * TODO: (rtobar) Maybe this code can be written more nicely, but always taking care that, if not in an illegal * state, then we should destroy the removed proxySupplier object */ boolean success = false; try { // Clean up callback for reconnection requests channelReconnectionCallback.disconnect(); // remove all filters and destroy the proxy supplier proxySupplier.remove_all_filters(); try { // Clean up callback for subscribed events if (corbaRef != null) { // this check avoids ugly "offshoot was not activated" messages in certain scenarios helper.getContainerServices().deactivateOffShoot(corbaObj); } } catch (AcsJContainerServicesEx e) { logger.log(Level.INFO, "Failed to Corba-deactivate NCSubscriber " + clientName, e); } logger.finer("Disconnected from NC '" + channelName + "'."); success = true; } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex1) { // this is OK, because someone else has already destroyed the remote resources logger.fine("No need to release resources for channel " + channelName + " because the NC has been destroyed already."); success = true; // } catch (Throwable thr) { // // TODO remove this hack // throw new AcsJStateMachineActionEx(thr); } finally { if (success) { // null the refs if everything was fine, or if we got the OBJECT_NOT_EXIST channelReconnectionCallback = null; corbaRef = null; corbaObj = null; } } super.destroyConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents); } protected void suspendAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { super.suspendAction(evtDispatcher, errRep, scInstance, derivedEvents); try { // See OMG NC spec 3.4.13.2. Server will continue to queue events. proxySupplier.suspend_connection(); } catch (org.omg.CosNotifyChannelAdmin.ConnectionAlreadyInactive ex) { throw new AcsJStateMachineActionEx(ex); } catch (org.omg.CosNotifyChannelAdmin.NotConnected ex) { throw new AcsJStateMachineActionEx(ex); } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex) { throw new AcsJStateMachineActionEx("Remote resources already destroyed.", ex); } } protected void resumeAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { try { proxySupplier.resume_connection(); } catch (org.omg.CosNotifyChannelAdmin.ConnectionAlreadyActive ex) { throw new AcsJStateMachineActionEx(ex); } catch (org.omg.CosNotifyChannelAdmin.NotConnected ex) { throw new AcsJStateMachineActionEx(ex); } super.resumeAction(evtDispatcher, errRep, scInstance, derivedEvents); } //////////////////////////////////////////////////////////////////////////////////////// /////////////////////// Various template method impls ////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// @Override protected boolean isTraceEventsEnabled() { return isTraceNCEventsEnabled; } @Override protected void logEventReceiveHandlerException(String eventName, String receiverClassName, Throwable thr) { LOG_NC_EventReceive_HandlerException.log(logger, channelName, getNotificationFactoryName(), eventName, receiverClassName, thr.toString()); } @Override protected void logEventProcessingTimeExceeded(String eventName, long logOcurrencesNumber) { LOG_NC_ProcessingTimeExceeded.log(logger, channelName, getNotificationFactoryName(), eventName, logOcurrencesNumber); } @Override protected void logEventProcessingTooSlowForEventRate(long numEventsDiscarded, String eventName) { LOG_NC_ReceiverTooSlow.log(logger, clientName, numEventsDiscarded, eventName, channelName, getNotificationFactoryName()); } @Override protected void logNoEventReceiver(String eventName) { // With server-side filtering set up, we should never get an unexpected event type. // Thus we log this problem. LOG_NC_EventReceive_NoHandler.log(logger, channelName, getNotificationFactoryName(), eventName); if (noEventReceiverListener != null) { noEventReceiverListener.noEventReceiver(eventName); } } /** * To be used only for unit tests. */ static interface NoEventReceiverListener { void noEventReceiver(String eventName); } /** * To be used only for unit tests. */ void setNoEventReceiverListener(NoEventReceiverListener noEventReceiverListener) { this.noEventReceiverListener = noEventReceiverListener; logger.info("Will notify test listener '" + noEventReceiverListener.getClass().getName() + "' of events without matching receiver."); } @Override protected void logQueueShutdownError(int timeoutMillis, int remainingEvents) { logger.info("Disconnecting from NC '" + channelName + "' before all events have been processed, in spite of " + timeoutMillis + " 500 ms timeout grace period. " + remainingEvents+ " events are now still in the queue and may continue to be processed by the receiver."); } @Override protected double getMaxProcessTimeSeconds(String eventName) { if (!handlerTimeoutMap.containsKey(eventName)) { // setup a timeout if it's undefined handlerTimeoutMap.put(eventName, DEFAULT_MAX_PROCESS_TIME_SECONDS); } //System.out.println("Using handlerTimeout=" + handlerTimeoutMap.get(eventName) + " for event " + eventName); double maxProcessTimeSeconds = handlerTimeoutMap.get(eventName); return maxProcessTimeSeconds; } /** * Adds a filter on the server-side supplier proxy that lets the given event type pass through. * <p> * Note that we derive the event type name from the simple class name of <code>struct</code>, * as done in other parts of ACS, which requires IDL event structs to have globally unique names * across IDL name spaces. * <p> * If <code>structClass</code> is <code>null</code> (generic subscription), * then "<code>*</code>" is used as the event type name, * which in ETCL is understood as a wildcard for all event type names. * * @param structClass * @throws AcsJEventSubscriptionEx */ @Override protected void notifyFirstSubscription(Class<?> structClass) throws AcsJEventSubscriptionEx { String eventTypeNameShort = ( structClass == null ? "*" : structClass.getSimpleName() ); try { int filterId = addFilter(eventTypeNameShort); subscriptionsFilters.put(eventTypeNameShort, filterId); } catch (AcsJCORBAProblemEx e) { throw new AcsJEventSubscriptionEx(e); } } @Override protected void notifySubscriptionRemoved(Class<?> structClass) throws AcsJEventSubscriptionEx { String eventTypeNameShort = ( structClass == null ? "*" : structClass.getSimpleName() ); try { proxySupplier.remove_filter(subscriptionsFilters.get(eventTypeNameShort)); subscriptionsFilters.remove(eventTypeNameShort); if (logger.isLoggable(AcsLogLevel.DELOUSE)) { NcFilterInspector insp = new NcFilterInspector( proxySupplier, channelName + "::" + clientName + "::ProxySupplier", logger); logger.log(AcsLogLevel.DELOUSE, "Removed filter for '" + eventTypeNameShort + "'. Current " + insp.getFilterInfo()); } } catch (FilterNotFound e) { throw new AcsJEventSubscriptionEx("Filter for '" + eventTypeNameShort + "' not found on the server side: ", e); } // If receivers is empty we just discard everything if (receivers.isEmpty()) { discardAllEvents(); } } @Override protected void notifyNoSubscription() { discardAllEvents(); } //////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////// Helper methods //////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// /** * Creates or reuses a shared server-side NC consumer admin object. * * @throws AcsJException */ private ConsumerAdmin getSharedAdmin() throws AcsJCORBAProblemEx, AcsJNarrowFailedEx { ConsumerAdmin ret = null; org.omg.CosNotifyChannelAdmin.ConsumerAdmin retBase = null; boolean created = false; int consumerAdminId = -1; AdminReuseCompatibilityHack adminReuseCompatibilityHack = new AdminReuseCompatibilityHack(channelName, logger); // @TODO (HSO): Why use a static lock here? This gives a false sense of safety in single-program unit tests, // while in real life we can have concurrent admin creation requests from different processes. synchronized(NCSubscriber.class) { // Check if we can reuse an already existing consumer admin for (int adminId : channel.get_all_consumeradmins()) { try { org.omg.CosNotifyChannelAdmin.ConsumerAdmin tmpAdmin = channel.get_consumeradmin(adminId); if (adminReuseCompatibilityHack.isSharedAdmin(tmpAdmin)) { // do some simple load balancing, so we use this shared admin only if it has space for more proxies // (the -1 goes because of the dummy proxy that is attached to the shared admin) if (tmpAdmin.push_suppliers().length - 1 < PROXIES_PER_ADMIN) { retBase = tmpAdmin; consumerAdminId = adminId; break; } } } catch (AdminNotFound e) { logger.log(AcsLogLevel.NOTICE, "Consumer admin with ID='" + adminId + "' not found for channel '" + channelName + "', " + "will continue anyway to search for shared consumer admins", e); } } // If no suitable consumer admin was found, we create a new one if (retBase == null) { // create a new consumer admin IntHolder consumerAdminIDHolder = new IntHolder(); // We use filters only on proxy objects, not on admin objects. // An admin object without filters will opt to pass all events. // We need a logical AND to be used when comparing the event passing decisions // made by the set of proxy supplier filters and by the admin object. InterFilterGroupOperator adminProxyFilterLogic = InterFilterGroupOperator.AND_OP; retBase = channel.new_for_consumers(adminProxyFilterLogic, consumerAdminIDHolder); consumerAdminId = consumerAdminIDHolder.value; created = true; } } // synchronize(NCSubscriber.class) ... try { // cast to TAO extension type ret = ConsumerAdminHelper.narrow(retBase); } catch (BAD_PARAM ex) { if (created) { retBase.destroy(); } LOG_NC_TaoExtensionsSubtypeMissing.log(logger, "ConsumerAdmin for channel " + channelName, ConsumerAdminHelper.id(), org.omg.CosNotifyChannelAdmin.ConsumerAdminHelper.id()); AcsJNarrowFailedEx ex2 = new AcsJNarrowFailedEx(ex); ex2.setNarrowType(ConsumerAdminHelper.id()); throw ex2; } if (created) { // @TODO: Remove this workaround once it is no longer needed. adminReuseCompatibilityHack.markAsSharedAdmin(ret); } LOG_NC_ConsumerAdminObtained_OK.log(logger, consumerAdminId, (created ? "created" : "reused"), clientName, channelName, getNotificationFactoryName()); return ret; } /** * Creates the proxy supplier (push-style, for structured events) * that lives in the Notify server process, managed by the consumer admin object, and * will later be connected to this client-side subscriber object. * * @throws AcsJCORBAProblemEx If creation of the proxy supplier failed. */ private StructuredProxyPushSupplier createProxySupplier() throws AcsJCORBAProblemEx { StructuredProxyPushSupplier ret = null; String errMsg = null; IntHolder proxyIdHolder = new IntHolder(); // will get assigned "a numeric identifier [...] that is unique among all proxy suppliers [the admin object] has created" String randomizedClientName = null; try { ProxySupplier proxy = null; while( proxy == null ) { // See the comments on Consumer#createConsumer() for a nice explanation of why this randomness is happening here randomizedClientName = Helper.createRandomizedClientName(clientName); try { proxy = sharedConsumerAdmin.obtain_named_notification_push_supplier(ClientType.STRUCTURED_EVENT, proxyIdHolder, randomizedClientName); } catch (NameAlreadyUsed e) { // Hopefully we won't run into this situation. Still, try to go on in the loop, // with a different client name next time. } catch (NameMapError e) { // Default to the unnamed version proxy = sharedConsumerAdmin.obtain_notification_push_supplier(ClientType.STRUCTURED_EVENT, proxyIdHolder); } } ret = StructuredProxyPushSupplierHelper.narrow(proxy); } catch (AdminLimitExceeded ex) { // See NC spec 3.4.15.10 // If the number of consumers currently connected to the channel with which the target ConsumerAdmin object is associated // exceeds the value of the MaxConsumers administrative property, the AdminLimitExceeded exception is raised. String limit = ex.admin_property_err.value.extract_string(); errMsg = "NC '" + channelName + "' is configured for a maximum of " + limit + " subscribers, which does not allow this client to subscribe."; } if (ret != null) { LOG_NC_SupplierProxyCreation_OK.log(logger, proxyIdHolder.value, clientName, randomizedClientName, channelName, getNotificationFactoryName()); } else { LOG_NC_SupplierProxyCreation_FAIL.log(logger, clientName, channelName, getNotificationFactoryName(), errMsg); AcsJCORBAProblemEx ex2 = new AcsJCORBAProblemEx(); ex2.setInfo("Failed to create proxy supplier on NC '" + channelName + "' for client '" + clientName + "': " + errMsg); throw ex2; } return ret; } /** * This method manages the filtering capabilities used to control subscriptions. * <p> * A constraint evaluates to true when both of the following conditions are true: * A member of the constraint's EventTypeSeq matches the message's event type. * The constraint expression evaluates to true. * * @return FilterID (see OMG NotificationService spec 3.2.4.1) * @throws AcsJCORBAProblemEx */ protected int addFilter(String eventTypeName) throws AcsJCORBAProblemEx { try { // Create the filter FilterFactory filterFactory = channel.default_filter_factory(); Filter filter = filterFactory.create_filter(getFilterLanguage()); // Information needed to construct the constraint expression object // (any domain, THE event type) // Note that TAO will internally convert the event type name // to the expression "$type_name=='<our_eventTypeName>'", // see orbsvcs/Notify/Notify_Constraint_Interpreter.cpp EventType[] t_info = { new EventType("*", eventTypeName) }; // The old Consumer class used 'getChannelDomain()' instead of "*"..? // Add constraint expression object to the filter String constraint_expr = ""; // no constraints other than the eventTypeName already given above ConstraintExp[] cexp = { new ConstraintExp(t_info, constraint_expr) }; filter.add_constraints(cexp); // Add the filter to the proxy and return the filter ID int filterId = proxySupplier.add_filter(filter); if (logger.isLoggable(AcsLogLevel.DELOUSE)) { NcFilterInspector insp = new NcFilterInspector( proxySupplier, channelName + "::" + clientName + "::ProxySupplier", logger); logger.log(AcsLogLevel.DELOUSE, "Added filter for '" + eventTypeName + "'. Current " + insp.getFilterInfo()); // NcFilterInspector insp2 = new NcFilterInspector( // sharedConsumerAdmin, channelName + "::" + clientName + "::Admin", logger); // logger.log(AcsLogLevel.DEBUG, "Admin filters: " + insp2.getFilterInfo()); } return filterId; } catch (org.omg.CosNotifyFilter.InvalidGrammar e) { Throwable cause = new Throwable("'" + eventTypeName + "' filter is invalid for the '" + channelName + "' channel: " + e.getMessage()); throw new alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx(cause); } catch (org.omg.CosNotifyFilter.InvalidConstraint e) { Throwable cause = new Throwable("'" + eventTypeName + "' filter is invalid for the '" + channelName + "' channel: " + e.getMessage()); throw new alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx(cause); } } /** * This method is used to discard all events. It is called when there are no * subscriptions left or if the {@link #removeSubscription()} method is * called with null as parameter. */ private void discardAllEvents() { // For safety, remove all filters in the server, clear the local references, // and put a dummy filter that filters out everything proxySupplier.remove_all_filters(); subscriptionsFilters.clear(); try { // If no filters are attached, the default behavior is to pass all events. // Thus we attach a dummy forwarding filter, to enable the one-filter-must-match behavior. addFilter("EVENT_TYPE_THAT_NEVER_MATCHES"); // TODO: It seems that once a filter was added, calling 'remove_all_filters' does not restore // the "pass all" behavior, cf. NCSubscriberTest#testServerSideEventTypeFiltering() comments. // Thus we may be able to delete this dummy filter again, although it seems a bit risky. } catch (AcsJCORBAProblemEx e) { logger.log(AcsLogLevel.ERROR, "Cannot add all-exclusive filter, we'll keep receiving events, but no handler will receive them"); } } /** * This method returns the notify service name as registered with the CORBA * Naming Service. This is normally equivalent to * <code>NotifyEventChannelFactory</code>. * * @return string */ protected String getNotificationFactoryName() { return helper.getNotificationFactoryNameForChannel(); } /** * * This method returns a string to the type of filter constraint language to * be used for filtering events, which is normally equivalent to * acsnc::FILTER_LANGUAGE_NAME (ETCL = Extended Trader Constraint Language). * * @return pointer to a constant string. */ protected String getFilterLanguage() { return alma.acsnc.FILTER_LANGUAGE_NAME.value; } //////////////////////////////////////////////////////////////////////////////////////// /////////////////////////// Corba callback methods //////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// /** * This method is called by the notification channel (supplier proxy) each time an event is received. * <p> * It is declared <code>final</code> because it is crucial for the functioning of the NC library * and thus cannot be overwritten by a subclass. * If for special purposes a notification of raw event reception is needed, * a subclass can implement {@link #push_structured_event_called(StructuredEvent)}, which gets called from this * method as the first thing it does. * @param structuredEvent * The structured event sent by a supplier. * @throws Disconnected If this subscriber is disconnected from the NC. * See NC spec 3.3.7.1: "if the invocation of push_structured_event upon a StructuredPushConsumer instance * by a StructuredProxyPushSupplier instance results in the Disconnected exception being raised, * the StructuredProxyPushSupplier will invoke its own disconnect_structured_push_supplier operation, * resulting in the destruction of that StructuredProxyPushSupplier instance." * This serves only as a backup mechanism, since normally we explicitly disconnect the subscriber. * * @see org.omg.CosNotifyComm.StructuredPushConsumerOperations#push_structured_event(org.omg.CosNotification.StructuredEvent) */ @Override public final void push_structured_event(StructuredEvent structuredEvent) throws Disconnected { boolean shouldProcessEvent = true; try { shouldProcessEvent = push_structured_event_called(structuredEvent); } catch (Throwable thr) { // ignore any exception, since push_structured_event_called is only meant for // notification, to enable special tests or other exotic purposes. // In this case we also keep shouldProcessEvent=true, just in case. // TODO: It may be better to treat the exception like shouldProcessEvent==false // since non-struct event data will cause more errors further down. } // got a subclass 'veto'? if (!shouldProcessEvent) { if (firstSubclassVeto) { logger.info("Event subscriber '" + getClass().getSimpleName() + "' handles one or more raw NC events itself, bypassing base class '" + NCSubscriber.class.getName() + "'. This non-standard behavior will not be logged again by this NCSubscriber."); firstSubclassVeto = false; } return; } if (isDisconnected()) { throw new Disconnected(); } Object convertedAny = anyAide.complexAnyToObject(structuredEvent.filterable_data[0].value); if (convertedAny == null) { // @TODO: compare with ACS-NC specs and C++ impl, and perhaps call generic receiver with null data, // if the event does not carry any data. LOG_NC_EventReceive_FAIL.log( logger, channelName, getNotificationFactoryName(), structuredEvent.header.fixed_header.event_type.type_name, "null"); } else { // An optimization: If the event type cannot match a typed or generic receiver // then we don't put it into the queue. We could improve this by checking for registered receivers already here... if (!eventType.isInstance(convertedAny) && !hasGenericReceiver()) { logNoEventReceiver(convertedAny.getClass().getName()); } EventDescription eventDesc = EventDescriptionHelper.extract(structuredEvent.remainder_of_body); if (isTraceEventsEnabled()) { LOG_NC_EventReceive_OK.log( logger, channelName, getNotificationFactoryName(), structuredEvent.header.fixed_header.event_type.type_name); } // let the base class deal with queue and dispatching to receiver processEventAsync(convertedAny, eventDesc); } } /** * Users can override this method to get notified of raw events, for additional statistics, * to handle event data given as a sequence of IDL structs (exceptional case in acssamp), * or for DynAny access (eventGUI). * <p> * Usually this method should not be overridden. * * @param structuredEvent * @return <code>true</code> if normal event processing should continue, * <code>false</code> if NCSubscriber should not process this event. */ protected boolean push_structured_event_called(StructuredEvent structuredEvent) { //System.out.println("********** got a call to push_structured_event **********"); return true; } /** * ACS does not provide an implementation of this method. * * @see org.omg.CosNotifyComm.StructuredPushConsumerOperations#disconnect_structured_push_consumer() * @throws NO_IMPLEMENT */ @Override public void disconnect_structured_push_consumer() { throw new NO_IMPLEMENT(); } /** * ACS does not provide an implementation of this method. * * @see org.omg.CosNotifyComm.NotifyPublishOperations#offer_change(org.omg.CosNotification.EventType[], * org.omg.CosNotification.EventType[]) * @throws NO_IMPLEMENT */ @Override public void offer_change(EventType[] added, EventType[] removed) throws InvalidEventType { throw new NO_IMPLEMENT(); } /** * @TODO: Perhaps integrate reconnection into the state machine. * * @see alma.acs.nc.ReconnectableParticipant#reconnect(gov.sandia.NotifyMonitoringExt.EventChannelFactory) */ @Override public void reconnect(EventChannelFactory ecf) { logger.log(AcsLogLevel.NOTICE, "Reconnecting subscriber with channel '" + channelName + "' after Notify Service recovery"); if (channel != null) { channel = helper.getNotificationChannel(ecf); if (channel == null) { logger.log(Level.WARNING, "Cannot reconnect to the channel '" + channelName + "'"); return; } } try { channel.set_qos(helper.getChannelProperties().getCDBQoSProps(channelName)); channel.set_admin(helper.getChannelProperties().getCDBAdminProps(channelName)); } catch (UnsupportedQoS e) { } catch (AcsJException e) { } catch (UnsupportedAdmin ex) { logger.warning(helper.createUnsupportedAdminLogMessage(ex)); } } //////////////////////////////////////////////////////////////////////////////////////// /////////////////////////// AdminReuseCompatibilityHack //////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// /** * Encapsulates the hack of using a dummy ProxyType.PUSH_ANY proxy to mark a shared consumer admin used by NCSubscriber * (or other next-gen subscribers), to distinguish it from non-reusable (subscriber-owned) admin objects * as they are used by the old-generation subscribers. * <p> * @TODO: Remove this class once the hack is no longer needed. */ public static class AdminReuseCompatibilityHack { public static final String DUMMY_SUPPLIER_PROXY_NAME_PREFIX = "dummyproxy"; private final String channelName; private final Logger logger; public AdminReuseCompatibilityHack(String channelName, Logger logger) { this.channelName = channelName; this.logger = logger; } /** * Creates a dummy proxy in the given consumer admin. * This dummy proxy is not of a StructuredProxyPushSupplier, like the rest of the proxies that are created for the NC in the admin objects. * This way we can recognize a shared consumer admin by looking at its proxies, and checking if their "MyType" property is ANY_EVENT. * This hack is only needed while we are in transition between the old and new NC classes, and should get removed once the old classes are not used * anymore (also not in C++ etc). * In addition to using a unique proxy type, we also use a recognizable name, * so that we can recognize the dummy proxy even when we only know its name, * as it happens when working with the TAO MC statistics, see {@link #isDummyProxy(String)}. * * @throws AcsJCORBAProblemEx */ public void markAsSharedAdmin(ConsumerAdmin consumerAdmin) throws AcsJCORBAProblemEx { try { // There should be only one dummy proxy per shared admin, but any two concurrent subscribers // should rather create a dummy proxy too many than getting an exception. Thus we make the name unique. String dummySupplierName = Helper.createRandomizedClientName(DUMMY_SUPPLIER_PROXY_NAME_PREFIX); consumerAdmin.obtain_named_notification_push_supplier(ClientType.ANY_EVENT, new IntHolder(), dummySupplierName); } catch (Exception ex) { // This ex could be AdminLimitExceeded, NameAlreadyUsed, NameMapError consumerAdmin.destroy(); AcsJCORBAProblemEx e2 = new AcsJCORBAProblemEx(ex); e2.setInfo("Coundn't attach dummy ANY_EVENT proxy to newly created shared admin consumer for channel '" + channelName + "'. Newly created shared admin is now destroyed."); throw e2; } } /** * Checks if a given proxy supplier (as obtained through the regular NC API) * is a dummy produced by this class. * * @return <code>true</code> if the given proxy is of type PUSH_ANY, * which is used only to mark a shared consumer admin. */ public static boolean isDummyProxy(ProxySupplier proxy) { return ( ProxyType.PUSH_ANY.equals(proxy.MyType()) ); } /** * Checks if a given proxy supplier (as obtained by name through the TAO MC extension API) is a dummy produced * by this class. * * @return <code>true</code> if the given proxy name starts with {@link #DUMMY_SUPPLIER_PROXY_NAME_PREFIX}, * which is used only to mark a shared consumer admin. */ public static boolean isDummyProxy(String proxyName) { return ( proxyName.startsWith(DUMMY_SUPPLIER_PROXY_NAME_PREFIX) ); } /** * @return <code>true</code> if our consumer admin is shared. In the future when all NC libs are ported, this should always be the case. */ public boolean isSharedAdmin(org.omg.CosNotifyChannelAdmin.ConsumerAdmin consumerAdmin) { boolean ret = false; int[] push_suppliers_ids = consumerAdmin.push_suppliers(); for(int proxyID: push_suppliers_ids) { try { ProxySupplier proxy = consumerAdmin.get_proxy_supplier(proxyID); if(isDummyProxy(proxy)) { ret = true; break; } } catch(ProxyNotFound e) { logger.log(AcsLogLevel.NOTICE, "Proxy with ID='" + proxyID + "' not found for consumer admin with ID='" + consumerAdmin.MyID() + "', " + "will continue anyway to search for shared consumer admins", e); } } return ret; } } }