/******************************************************************************* * Copyright © 2012-2015 eBay Software Foundation * This program is dual licensed under the MIT and Apache 2.0 licenses. * Please see LICENSE for more information. *******************************************************************************/ package com.ebay.jetstream.event.channel.messaging; /** * A OutboundMessagingChannel represents messaging channel deployed at the egress * of an application. It is provided an address that contains a list of Jetstream * Message service topics to publish to. It receives events from it's event source * and publishes the same on the message bus using Jetstream Message Service. * It subscribes to Message Service internal state advisory. The advisory coming * out of the message service provides it with stop sending and resume advise * messages. In response to stop sending message it raises an alarm signalling * overrun condition and upon receving resume advise from message service it * clears the alarm. When a JetstreamEvent arrives to be sent on the bus, it is * examined to see if it contains affinityKeys. If it does, the affinity key is * extracted, the JetstreamEvent is inserted in to a Jetstream Message of type any, * the affinity key set on the message and subbsequently the message is sent * to message service to be send out on the bus. * * @author shmurthy@ebay.com * */ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; import com.ebay.jetstream.application.JetstreamApplication; import com.ebay.jetstream.config.ContextBeanChangedEvent; import com.ebay.jetstream.counter.LongCounter; import com.ebay.jetstream.event.EventException; import com.ebay.jetstream.event.JetstreamEvent; import com.ebay.jetstream.event.JetstreamReservedKeys; import com.ebay.jetstream.event.RetryEventCode; import com.ebay.jetstream.event.channel.AbstractOutboundChannel; import com.ebay.jetstream.event.channel.ChannelAddress; import com.ebay.jetstream.event.channel.ChannelAlarm; import com.ebay.jetstream.event.support.ErrorManager; import com.ebay.jetstream.management.Management; import com.ebay.jetstream.messaging.MessageService; import com.ebay.jetstream.messaging.exception.MessageServiceException; import com.ebay.jetstream.messaging.interfaces.IMessageListener; import com.ebay.jetstream.messaging.messagetype.AdvisoryMessage; import com.ebay.jetstream.messaging.messagetype.AdvisoryMessage.AdvisoryCode; import com.ebay.jetstream.messaging.messagetype.Any; import com.ebay.jetstream.messaging.messagetype.JetstreamMessage; import com.ebay.jetstream.messaging.topic.JetstreamTopic; import com.ebay.jetstream.notification.AlertListener.AlertStrength; @ManagedResource(objectName = "Event/Channel", description = "Outbound messaging component") public class OutboundMessagingChannel extends AbstractOutboundChannel implements IMessageListener { private static final Logger LOGGER = LoggerFactory.getLogger("com.ebay.jetstream.event.channel.messaging"); private final static String affinityKey = JetstreamReservedKeys.MessageAffinityKey.toString(); private final static String bcastMsg = JetstreamReservedKeys.EventBroadCast.toString(); private final AtomicBoolean m_alarmRaised = new AtomicBoolean(false); private MessagingChannelAddress m_address; private final LongCounter m_retryAdvisoryEvents = new LongCounter(); private final ConcurrentHashMap<String, AtomicBoolean> m_alarmState = new ConcurrentHashMap<String, AtomicBoolean>(); private final AtomicLong m_eventSentToAdviceListener = new AtomicLong(0); private boolean m_constructEventHolder = false; private AtomicBoolean m_channelOpened = new AtomicBoolean(false); private ErrorManager m_errors = new ErrorManager(); private int retryCount = 3; public int getRetryCount() { return retryCount; } public void setRetryCount(int retryCount) { this.retryCount = retryCount; } public OutboundMessagingChannel() { } public void afterPropertiesSet() throws Exception { List<JetstreamTopic> publishingTopics = m_address.getChannelJetstreamTopics(); for (JetstreamTopic topic : publishingTopics) { MessageService.getInstance().prepareToPublish(topic); } } public ErrorManager getErrors() { return m_errors; } private boolean areAllContextsInAlarmCondition() { Iterator<Entry<String, AtomicBoolean>> itr = m_alarmState.entrySet().iterator(); while (itr.hasNext()) { Entry<String, AtomicBoolean> entry = itr.next(); if (!entry.getValue().get()) return false; } return true; } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#close() */ public void close() throws EventException { LOGGER.info( "Closing Outbound Messaging Chanel"); if (!m_channelOpened.get()) return; super.close(); Management.removeBeanOrFolder(getBeanName(), this); m_channelOpened.set(false); if (getAdviceListener() != null) { List<JetstreamTopic> publishingTopics = m_address .getChannelJetstreamTopics(); for (JetstreamTopic topic : publishingTopics) { try { MessageService.getInstance().unsubscribe( new JetstreamTopic(topic.getRootContext() + "/InternalStateAdvisory"), this); } catch (MessageServiceException e) { m_errors.registerError(e); String errMsg = "Error Unsubscribing for Topic - " + topic.getTopicName(); errMsg += " - "; errMsg += e.getMessage(); LOGGER.error( errMsg); } catch (Throwable e) { m_errors.registerError(e); LOGGER.error( "Error unSubscribing for Topic - " + topic.getTopicName() + e.getMessage()); } } } } private JetstreamEvent constructTEventHolder(JetstreamEvent event) { if (event != null) { JetstreamEvent holderEvent = new JetstreamEvent(); holderEvent.put(JetstreamReservedKeys.JetstreamEventHolder.toString(), event); holderEvent.put(JetstreamReservedKeys.EventType.toString(), event.getEventType()); // insert source application to DTE String eventSource = null; if (event.get(JetstreamReservedKeys.EventSource.toString()) != null) { eventSource = (String) event.get(JetstreamReservedKeys.EventSource.toString()); } else { eventSource = JetstreamApplication.getInstance().getApplicationInformation().getApplicationName(); } holderEvent.put(JetstreamReservedKeys.EventSource.toString(), eventSource); return holderEvent; } return event; } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#flush() */ public void flush() throws EventException { // This is a NOOP for this channel } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#getAddress() */ public ChannelAddress getAddress() { return m_address; } public long getRetryAdvisoryEvents() { return m_retryAdvisoryEvents.get(); } private void incrementAdviceListenerCount() { if (m_eventSentToAdviceListener.incrementAndGet() < 0) { m_eventSentToAdviceListener.set(0); } } private void incrementRetryAdvisoryEvents() { m_retryAdvisoryEvents.increment(); } public boolean isConstructEventHolder() { return m_constructEventHolder; } /** * @param context * @return */ private boolean matchesMyPublishingContexts(String context) { List<JetstreamTopic> publishingTopics = m_address.getChannelJetstreamTopics(); for (JetstreamTopic topic : publishingTopics) { if (context.equals(topic.getRootContext())) return true; } return false; } /* * (non-Javadoc) * * @see com.ebay.jetstream.messaging.JetstreamMessageListener#onMessage(com.ebay.jetstream.messaging.JetstreamMessage) */ public void onMessage(JetstreamMessage m) { if (m instanceof AdvisoryMessage) { AdvisoryMessage advise = (AdvisoryMessage) m; if (advise.isStopSending() && matchesMyPublishingContexts(advise.getAdvisoryTopic() .getRootContext())) { LOGGER.warn( "Outbound Messaging Channel raising alarm"); setTopicAlarmState(advise.getAdvisoryTopic().getRootContext()); incrementAlarmsRaisedCounter(); if (areAllContextsInAlarmCondition()) { m_alarmRaised.set(true); getAlarmListener().alarm(ChannelAlarm.OVERRUN); postAlert("All Contexts in Alarm Condition - ", AlertStrength.ORANGE); } } else if (advise.isResumeSending() && matchesMyPublishingContexts(advise.getAdvisoryTopic() .getRootContext())) { LOGGER.info( "Outbound Messaging Channel clearing alarm for topic-> " + advise.getAdvisoryTopic().getRootContext()); resetTopicAlarmState(advise.getAdvisoryTopic().getRootContext()); if (!areAllContextsInAlarmCondition()) { m_alarmRaised.set(false); getAlarmListener().alarm(ChannelAlarm.CLEAR); postAlert("Alarm Condition Cleared - ", AlertStrength.YELLOW); incrementAlarmsClearedCounter(); } } else if (advise.isResendMessage()) { JetstreamEvent evt = (JetstreamEvent) ((Any) ((AdvisoryMessage) m) .getUndeliveredMsg()).getObject(); try { evt = evt.clone(); } catch (CloneNotSupportedException e) { m_errors.registerError(e); LOGGER.debug( "Failed to clone event : Exception = " + e.getLocalizedMessage()); this.incrementEventDroppedCounter(); return; } List<String> topics = m_address.getChannelTopics(); String topic = ((AdvisoryMessage) m).getAdvisoryTopic().toString(); // Fix issue when multiple OMCs subscribe different topics under same context. // Each OMC will receive broadcast advisory events. if (topic == null || topics.contains(topic)) { evt.put(JetstreamReservedKeys.EventReplayTopic.toString(), topic); Integer retryCount = 1; if (evt != null) { if (evt.containsKey(JetstreamReservedKeys.RetryCount .toString())) { retryCount = (Integer) evt .get(JetstreamReservedKeys.RetryCount .toString()); retryCount++; } evt.put(JetstreamReservedKeys.RetryCount.toString(), retryCount); sendToAdviceListener(evt, RetryEventCode.MSG_RETRY, AdvisoryCode.RESEND_MESSAGE.toString()); incrementRetryAdvisoryEvents(); } } } } } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#open() */ public void open() throws EventException { if (m_channelOpened.get()) return; super.open(); LOGGER.info( "Opening Outbound Messaging Channel"); Management.removeBeanOrFolder(getBeanName(), this); Management.addBean(getBeanName(), this); if (getAdviceListener() != null) { List<JetstreamTopic> publishingTopics = m_address .getChannelJetstreamTopics(); for (JetstreamTopic topic : publishingTopics) { subscribe(new JetstreamTopic(topic.getRootContext() + "/InternalStateAdvisory")); // subscribe to advisory // messages } } m_channelOpened.set(true); } /** * */ @ManagedOperation public void resetStats() { super.resetStats(); m_retryAdvisoryEvents.set(0); } /** * * Resets the rootcontext in hashmap to false indicating alarm has been cleared on the topic * * @param rootContext */ private void resetTopicAlarmState(String rootContext) { try { if (rootContext != null) { AtomicBoolean alarmState = m_alarmState.get(rootContext); if (alarmState != null) alarmState.set(false); } } catch (Exception e) { // if root context does not exist we will get NPE - we will swallow it. } } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.EventSink#sendEvent(com.ebay.jetstream.event.JetstreamEvent) */ public void sendEvent(JetstreamEvent event) throws EventException { incrementEventRecievedCounter(); List<JetstreamTopic> publishingTopics = m_address.getChannelJetstreamTopics(); String[] forwardingTopics = event.getForwardingTopics(); for (JetstreamTopic topic : publishingTopics) { if (forwardingTopics != null) { boolean bFound = false; String strSeekName = topic.getTopicName(); for (String strTopic : forwardingTopics) { if (strTopic.equals(strSeekName)) { bFound = true; break; } } if (!bFound) continue; } if (LOGGER.isDebugEnabled()) { LOGGER.debug( "eventSource=" + getBeanName() + "&eventId = " + event.getEventId() + "&topic=" + topic); } Any any = null; if (isConstructEventHolder()) { JetstreamEvent holderEvent = constructTEventHolder(event); any = new Any(holderEvent); } else { any = new Any(event); } any.setPriority(JetstreamMessage.HI_PRIORITY); if (event.containsKey(affinityKey)) { Object affkey = event.get(affinityKey); if (affkey == null) { incrementEventDroppedCounter(); if (LOGGER.isDebugEnabled()) LOGGER.debug(" Affinity Key passed in is null"); return; } any.setAffinityKey(affkey); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "sending on topic - " + topic.getTopicName() + " " + ": Affinity Key = " + event.get(affinityKey).toString() + " EVENT - " + event.toString()); } } // we assume that caller will not set affinity key and bcast flag at the same time // if affinity key is set it will override bcast if (event.getMetaData(bcastMsg) != null) { any.setBroadcastMessage(Boolean.parseBoolean((String) event.getMetaData(bcastMsg))); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "sending on topic - " + topic.getTopicName() + " " + ": BroadCast Key = " + event.getMetaData(bcastMsg).toString() + " EVENT - " + event.toString()); } } try { MessageService.getInstance().publish(topic, any); setLastEvent(event); incrementEventSentCounter(); } catch (MessageServiceException mse) { m_errors.registerError(mse); String errMsg = "Error Publishing Message for Topic - " + topic.getTopicName(); // NOPMD errMsg += " - "; errMsg += mse.getMessage(); if (LOGGER.isWarnEnabled()) { LOGGER.warn( errMsg); } sendToAdviceListener(event, RetryEventCode.UNKNOWN, errMsg); incrementEventDroppedCounter(); if (mse.getError() == MessageServiceException.BUFFER_FULL && !m_alarmRaised.get()) { setTopicAlarmState(topic.getRootContext()); if (areAllContextsInAlarmCondition()) { getAlarmListener().alarm(ChannelAlarm.OVERRUN); postAlert("MessageService Buffer full condition - ", AlertStrength.YELLOW); LOGGER.info( "Outbound Messaging Channel raising alarm"); m_alarmRaised.set(true); incrementAlarmsRaisedCounter(); } } } catch (Throwable t) { m_errors.registerError(t); String errMsg = "Error Publishing Message for Topic - " + topic.getTopicName(); errMsg += " - "; errMsg += t.getMessage(); sendToAdviceListener(event, RetryEventCode.UNKNOWN, errMsg); incrementEventDroppedCounter(); if (LOGGER.isDebugEnabled()) LOGGER.debug( errMsg); } } } /** * @param event * @param code * @param msg */ private void sendToAdviceListener(JetstreamEvent event, RetryEventCode code, String msg) { try { if (getAdviceListener() != null) { if (event.containsKey(JetstreamReservedKeys.RetryCount.toString())) { Integer retryCount = (Integer) event.get(JetstreamReservedKeys.RetryCount.toString()); if (retryCount > getRetryCount()) { LOGGER.info( "Unable to deliver this event so dropping it.." + event.getEventId()); m_totalEventsDropped.increment(); return; } } getAdviceListener().retry(event, code, msg); incrementAdviceListenerCount(); } } catch (Throwable e) { m_errors.registerError(e); LOGGER.debug( e.getLocalizedMessage()); } } /** * @param address */ public void setAddress(ChannelAddress address) { if (m_address != null) { m_address.setChannelTopics(((MessagingChannelAddress) address).getChannelTopics()); } else m_address = (MessagingChannelAddress) address; m_alarmState.clear(); for (JetstreamTopic topic : m_address.getChannelJetstreamTopics()) { m_alarmState.put(topic.getRootContext(), new AtomicBoolean(false)); } } public void setConstructEventHolder(boolean constructEventHolder) { m_constructEventHolder = constructEventHolder; } /** * * Sets the rootcontext in hashmap to true indicating there is an alarm on this topic * * @param rootContext */ private void setTopicAlarmState(String rootContext) { try { if (rootContext != null) { AtomicBoolean alarmState = m_alarmState.get(rootContext); if (alarmState != null) alarmState.set(true); } } catch (Exception e) { m_errors.registerError(e); // we might get a NPE if rootContext is not in map - swallow it } } public void shutDown() { close(); } /** * @param topic */ private void subscribe(JetstreamTopic topic) { try { LOGGER.info( "Subscribing to Topic - " + topic.getTopicName() + " on Outbound Messaging Channel"); MessageService.getInstance().subscribe(topic, this); } catch (MessageServiceException e) { m_errors.registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } catch (Exception e) { m_errors.registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } } @Override public String toString() { return getBeanName(); } @Override public void processApplicationEvent(ApplicationEvent event) { if (event instanceof ContextBeanChangedEvent) { ContextBeanChangedEvent bcInfo = (ContextBeanChangedEvent) event; // Calculate changes if (bcInfo.isChangedBean(getAddress())) { setAddress((ChannelAddress) bcInfo.getChangedBean()); } } } @Override public int getPendingEvents() { return 0; } }