/******************************************************************************* * 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 InboundMessagingChannel represents messaging channel deployed at the ingress * of an application. It is provided an address that contains a list of Jetstream * Message service topics to subscribe to. It receives events from the Message Service * and delivers to it's deplyed eventSinks. It also handles pause and resume * conditions as part of which it will unsubscribe and subscribe to the * configured Jetstream Topics * * @author shmurthy@ebay.com * */ import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; 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.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.RetryEventCode; import com.ebay.jetstream.event.advice.Advice; import com.ebay.jetstream.event.channel.AbstractInboundChannel; import com.ebay.jetstream.event.channel.ChannelAddress; 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.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 = "Inbound messaging component") public class InboundMessagingChannel extends AbstractInboundChannel implements IMessageListener { private static final Logger LOGGER = LoggerFactory.getLogger("com.ebay.jetstream.event.channel.messaging"); enum Signal { PAUSE, RESUME }; private static final String LOGGING_NAME = "InboundMessagingChannel"; private int m_maxPauseQueueSize = 300; private int m_pauseQueueDrainRate = 1000; // 20 per sec private MessagingChannelAddress m_address; private Advice m_adviceListener; // cache to store received events once we have received a pause. The cache // will be emptied on // reception of resume private final ConcurrentLinkedQueue<JetstreamEvent> m_pauseCache = new ConcurrentLinkedQueue<JetstreamEvent>(); private final LongCounter m_eventSentToAdviceListener = new LongCounter(); private AtomicBoolean m_channelOpened = new AtomicBoolean(false); private int m_shutDownEventsSent; private final AtomicBoolean m_shutdownStatus = new AtomicBoolean(false); private long m_waitTimeBeforeShutdown = 5000; public long getWaitTimeBeforeShutdown() { return m_waitTimeBeforeShutdown; } public void setWaitTimeBeforeShutdown(long m_waitTimeBeforeShutdown) { this.m_waitTimeBeforeShutdown = m_waitTimeBeforeShutdown; } public InboundMessagingChannel() { } public void afterPropertiesSet() throws Exception { } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#close() */ @Override public void close() throws EventException { if (!m_channelOpened.get()) return; super.close(); LOGGER.info( "Closing Inbound Messaging Channel"); Management.removeBeanOrFolder(getBeanName(), this); m_eventsReceivedPerSec.destroy(); m_eventsSentPerSec.destroy(); List<JetstreamTopic> channelTopics = m_address .getChannelJetstreamTopics(); for (JetstreamTopic topic : channelTopics) { try { unSubscribe(topic); } catch (Throwable e) { registerError(e); throw new EventException(e.getMessage()); } } m_channelOpened.set(false); } private void fireEvent(JetstreamEvent evt) { super.fireSendEvent(evt); } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#flush() */ public void flush() throws EventException { // This is NOOP for us } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#getAddress() */ public ChannelAddress getAddress() { return m_address; } public Advice getAdviceListener() { return m_adviceListener; } @Override public int getPendingEvents() { return m_pauseCache.size(); } /** * * @return */ public long getEventSentToAdviceListener() { return m_eventSentToAdviceListener.get(); } public int getMaxPauseQueueSize() { return m_maxPauseQueueSize; } public int getPauseQueueDrainRate() { return m_pauseQueueDrainRate; } /** * @return the pauseQueueSize */ public long getPauseQueueSize() { return m_pauseCache.size(); } /* * (non-Javadoc) * * @see * com.ebay.jetstream.messaging.JetstreamMessageListener#onMessage(com.ebay * .jetstream.messaging.JetstreamMessage) */ public void onMessage(JetstreamMessage m) { if (m instanceof Any && ((Any) m).getObject() instanceof JetstreamEvent) { incrementEventRecievedCounter(); JetstreamEvent event = (JetstreamEvent) ((Any) m).getObject(); if (LOGGER.isDebugEnabled()) LOGGER.debug( event.toString()); // if we are paused then we will put in our pause cache else forward // the event downstream if (isPaused() && !m_shutdownStatus.get()) { if (m_pauseCache.size() < m_maxPauseQueueSize) { m_pauseCache.offer(event); } else { incrementEventDroppedCounter(); } } else { try { fireEvent(event); incrementEventSentCounter(); setLastEvent(event); } catch (EventException exe) { sendToAdviceListener(event, RetryEventCode.PAUSE_RETRY, exe.getLocalizedMessage()); incrementEventDroppedCounter(); } catch (Throwable t) { registerError(t); incrementEventDroppedCounter(); LOGGER.warn( t.getLocalizedMessage()); return; } // now see if pause cache has some elements to be sent // downstream. while (!m_pauseCache.isEmpty()) { event = m_pauseCache.poll(); try { fireEvent(event); incrementEventSentCounter(); } catch (Throwable t) { sendToAdviceListener(event, RetryEventCode.PAUSE_RETRY, t.getLocalizedMessage()); registerError(t); LOGGER.warn( t.getLocalizedMessage()); incrementEventDroppedCounter(); } } } } } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.ChannelOperations#open() */ @Override public void open() throws EventException { if (m_channelOpened.get()) return; super.open(); LOGGER.info( "Opening Inbound Messaging Channel"); Management.removeBeanOrFolder(getBeanName(), this); Management.addBean(getBeanName(), this); List<JetstreamTopic> channelTopics = m_address .getChannelJetstreamTopics(); for (JetstreamTopic topic : channelTopics) { try { subscribe(topic); } catch (Throwable t) { registerError(t); LOGGER.error( "Error Opening Inbound Messaging Channel" + t.getLocalizedMessage()); } } m_channelOpened.set(true); } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.InboundChannel#pause() */ @Override @ManagedOperation public void pause() throws EventException { if (isPaused()) { LOGGER.warn( "InboundMessagingChannel " + getBeanName() + " could not be paused. It is already in paused state", "InboundMessagingChannel" + ".pause()"); return; } notifyProducer(Signal.PAUSE); changeState(ChannelOperationState.PAUSE); } void notifyProducer(Signal signal) { List<JetstreamTopic> channelTopics = m_address .getChannelJetstreamTopics(); for (JetstreamTopic topic : channelTopics) { try { if (Signal.PAUSE.equals(signal)) { MessageService.getInstance().pause(topic); } else { MessageService.getInstance().resume(topic); } } catch (Throwable e) { registerError(e); throw new EventException(e.getMessage()); } } if (Signal.PAUSE.equals(signal)) { postAlert(this.getBeanName() + " is being paused ", AlertStrength.RED); LOGGER.warn( " Inbound Messaging Channel paused."); } else { postAlert(getBeanName() + " is being resumed", AlertStrength.YELLOW); LOGGER.warn( "Resuming Inbound Messaging Channel"); } } @Override protected void processApplicationEvent(ApplicationEvent event) { if (event instanceof ContextBeanChangedEvent) { ContextBeanChangedEvent bcInfo = (ContextBeanChangedEvent) event; // Calculate changes if (bcInfo.isChangedBean(getAddress())) { LOGGER.info( "Received new configuration for InboundMessageChannel - " + getBeanName()); try { close(); } catch (Throwable e) { registerError(e); LOGGER.error( "Error closing InboundMessagingChannel while applying new config - " + e.getMessage()); } setAddress((ChannelAddress) bcInfo.getChangedBean()); try { open(); } catch (Throwable e) { registerError(e); // swallow the exception and log LOGGER.error( "Error opening InboundMessagingChannel while applying new config - " + e.getMessage()); } } } } /* * (non-Javadoc) * * @see com.ebay.jetstream.event.channel.InboundChannel#resume() */ @Override @ManagedOperation public void resume() throws EventException { if (!isPaused()) { LOGGER.warn( "InboundMessagingChannel " + getBeanName() + " could not be resumed. It is already in resume state", "InboundMessagingChannel" + ".pause()"); return; } // if we are in paused state then first empty the pause Cache which // could have received events // that were in the pipeline when we unsubscribed in response to a // pause. if (isPaused() && !m_shutdownStatus.get()) { changeState(ChannelOperationState.RESUME); while (!m_pauseCache.isEmpty()) { JetstreamEvent event = m_pauseCache.poll(); try { fireEvent(event); setLastEvent(event); incrementEventSentCounter(); } catch (Throwable t) { registerError(t); incrementEventDroppedCounter(); } // throttle the drain of the pause queue here so our sink is not // swamped resulting in a pause again try { Thread.sleep(1000 / m_pauseQueueDrainRate); } catch (InterruptedException e) { } } notifyProducer(Signal.RESUME); } } private void sendToAdviceListener(JetstreamEvent event, RetryEventCode code, String msg) { if (m_adviceListener != null) { m_adviceListener.retry(event, code, msg); incrementAdviceListenerCount(); } } /** * @param address */ public void setAddress(ChannelAddress address) { m_address = (MessagingChannelAddress) address; } public void setAdviceListener(Advice adviceListener) { m_adviceListener = adviceListener; } public void setMaxPauseQueueSize(int maxPauseQueueSize) { m_maxPauseQueueSize = maxPauseQueueSize; } public void setPauseQueueDrainRate(int pauseQueueDrainRate) { m_pauseQueueDrainRate = pauseQueueDrainRate; } @Override public void shutDown() { m_shutdownStatus.set(true); // now issue a pause to message service. // this will allow traffic to flow in. notifyProducer(Signal.PAUSE); try { Thread.sleep(m_waitTimeBeforeShutdown); } catch (InterruptedException e) { } if (m_pauseCache != null) { JetstreamEvent event = null; while (!m_pauseCache.isEmpty()) { event = m_pauseCache.poll(); try { fireEvent(event); incrementEventSentCounter(); } catch (EventException e) { registerError(e, event); sendToAdviceListener(event, RetryEventCode.PAUSE_RETRY, e.getMessage()); } catch (Throwable t) { registerError(t); LOGGER.error( t.getLocalizedMessage()); } m_shutDownEventsSent++; } close(); LOGGER.warn( m_shutDownEventsSent + " events drained from its queue due to graceful shutdown - " + "final events received = " + getTotalEventsReceived() + "final events sent = " + getTotalEventsSent() + "final total events dropped =" + getTotalEventsDropped(), LOGGING_NAME); } } /** * @param topic * @throws Exception */ private void subscribe(JetstreamTopic topic) throws Exception { try { LOGGER.info( "Subscribing to Topic - " + topic.getTopicName() + " on Inbound Messaging Channel"); MessageService.getInstance().subscribe(topic, this); } catch (MessageServiceException e) { registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } catch (Throwable e) { registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } } /** * @param topic * @throws Exception */ private void unSubscribe(JetstreamTopic topic) throws Exception { try { LOGGER.info( "Unsubscribing to Topic - " + topic.getTopicName() + " on Inbound Messaging Channel"); MessageService.getInstance().unsubscribe(topic, this); } catch (MessageServiceException e) { registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } catch (Exception e) { registerError(e); LOGGER.error( "Error Subscribing for Topic - " + topic.getTopicName() + e.getMessage()); } } }