All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.datatorrent.lib.io.jms.AbstractJMSInputOperator Maven / Gradle / Ivy

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.datatorrent.lib.io.jms;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;

import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.Topic;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.apex.malhar.lib.wal.FSWindowDataManager;
import org.apache.apex.malhar.lib.wal.WindowDataManager;
import org.apache.commons.lang.mutable.MutableLong;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import com.datatorrent.api.Context;
import com.datatorrent.api.Context.OperatorContext;
import com.datatorrent.api.DefaultOutputPort;
import com.datatorrent.api.InputOperator;
import com.datatorrent.api.Operator;
import com.datatorrent.api.Operator.ActivationListener;
import com.datatorrent.api.annotation.OperatorAnnotation;
import com.datatorrent.lib.counters.BasicCounters;
import com.datatorrent.netlet.util.DTThrowable;

/**
 * This is the base implementation of a JMS input operator.
* Subclasses must implement the method that converts JMS messages into tuples for emission. *

* The operator acts as a listener for JMS messages. When there is a message available in the message bus, * {@link #onMessage(Message)} is called which buffers the message into a holding buffer. This is asynchronous.
* {@link #emitTuples()} retrieves messages from holding buffer and processes them. *

* Important: The {@link FSWindowDataManager} makes the operator fault tolerant as * well as idempotent. If {@link WindowDataManager.NoopWindowDataManager} is set on the operator then * it will not be fault-tolerant as well. *

* Configurations:
* bufferSize: Controls the holding buffer size.
* consumerName: Name that identifies the subscription.
* * @param type of tuple emitted * @displayName Abstract JMS Input * @category Messaging * @tags jms, input operator * @since 0.3.2 */ @OperatorAnnotation(checkpointableWithinAppWindow = false) public abstract class AbstractJMSInputOperator extends JMSBase implements InputOperator, ActivationListener, MessageListener, ExceptionListener, Operator.IdleTimeHandler, Operator.CheckpointListener { protected static final int DEFAULT_BUFFER_SIZE = 10 * 1024; // 10k //Configurations: @Min(1) protected int bufferSize = DEFAULT_BUFFER_SIZE; private String consumerName; protected transient ArrayBlockingQueue holdingBuffer; protected final transient Map currentWindowRecoveryState; protected transient Message lastMsg; private transient MessageProducer replyProducer; private transient MessageConsumer consumer; @NotNull private final BasicCounters counters; private transient Context.OperatorContext context; private transient long spinMillis; private final transient AtomicReference throwable; @NotNull protected WindowDataManager windowDataManager; private transient long[] operatorRecoveredWindows; protected transient long currentWindowId; protected transient int emitCount; private final transient Set pendingAck; private final transient Lock lock; public final transient DefaultOutputPort output = new DefaultOutputPort(); public AbstractJMSInputOperator() { counters = new BasicCounters(MutableLong.class); throwable = new AtomicReference(); pendingAck = Sets.newHashSet(); windowDataManager = new FSWindowDataManager(); lock = new Lock(); //Recovery state is a linked hash map to maintain the order of tuples. currentWindowRecoveryState = Maps.newLinkedHashMap(); holdingBuffer = new ArrayBlockingQueue(bufferSize) { private static final long serialVersionUID = 201411151139L; @SuppressWarnings("Contract") @Override public boolean add(Message message) { synchronized (lock) { try { return messageConsumed(message) && super.add(message); } catch (JMSException e) { LOG.error("message consumption", e); throwable.set(e); throw new RuntimeException(e); } } } }; } /** * Implementation of {@link MessageListener} interface.
* Whenever there is message available in the message bus this will get called. * * @param message */ @Override public final void onMessage(Message message) { holdingBuffer.add(message); sendReply(message); } /** * If getJMSReplyTo is set then send message back to reply producer. * * @param message */ protected void sendReply(Message message) { try { if (message.getJMSReplyTo() != null) { // Send reply only if the replyTo destination is set replyProducer.send(message.getJMSReplyTo(), getSession().createTextMessage("Reply: " + message.getJMSMessageID())); } } catch (JMSException ex) { LOG.error(ex.getLocalizedMessage()); throwable.set(ex); throw new RuntimeException(ex); } } /** * Implementation of {@link ExceptionListener} * * @param ex */ @Override public void onException(JMSException ex) { cleanup(); LOG.error(ex.getLocalizedMessage()); throwable.set(ex); throw new RuntimeException(ex); } @Override public void setup(OperatorContext context) { this.context = context; spinMillis = context.getValue(OperatorContext.SPIN_MILLIS); counters.setCounter(CounterKeys.RECEIVED, new MutableLong()); counters.setCounter(CounterKeys.REDELIVERED, new MutableLong()); windowDataManager.setup(context); try { operatorRecoveredWindows = windowDataManager.getWindowIds(context.getId()); if (operatorRecoveredWindows != null) { Arrays.sort(operatorRecoveredWindows); } } catch (IOException e) { throw new RuntimeException("fetching windows", e); } } /** * This method is called when a message is added to {@link #holdingBuffer} and can be overwritten by subclasses * if required. This is called by the JMS thread not Operator thread. * * @param message * @return message is accepted. * @throws javax.jms.JMSException */ protected boolean messageConsumed(Message message) throws JMSException { if (message.getJMSRedelivered() && pendingAck.contains(message.getJMSMessageID())) { counters.getCounter(CounterKeys.REDELIVERED).increment(); LOG.warn("IGNORING: Redelivered Message {}", message.getJMSMessageID()); return false; } pendingAck.add(message.getJMSMessageID()); MutableLong receivedCt = counters.getCounter(CounterKeys.RECEIVED); receivedCt.increment(); LOG.debug("message id: {} buffer size: {} received: {}", message.getJMSMessageID(), holdingBuffer.size(), receivedCt.longValue()); return true; } /** * Implement ActivationListener Interface. * @param ctx */ @Override public void activate(OperatorContext ctx) { try { super.createConnection(); replyProducer = getSession().createProducer(null); consumer = (isDurable() && isTopic()) ? getSession().createDurableSubscriber((Topic)getDestination(), consumerName) : getSession().createConsumer(getDestination()); consumer.setMessageListener(this); } catch (JMSException ex) { throw new RuntimeException(ex); } } /** * Implementation of {@link Operator} interface. */ @Override public void beginWindow(long windowId) { currentWindowId = windowId; if (windowId <= windowDataManager.getLargestRecoveryWindow()) { replay(windowId); } } protected void replay(long windowId) { try { @SuppressWarnings("unchecked") Map recoveredData = (Map)windowDataManager.load(context.getId(), windowId); if (recoveredData == null) { return; } for (Map.Entry recoveredEntry : recoveredData.entrySet()) { pendingAck.add(recoveredEntry.getKey()); emit(recoveredEntry.getValue()); } } catch (IOException e) { throw new RuntimeException("replay", e); } } @Override public void emitTuples() { if (currentWindowId <= windowDataManager.getLargestRecoveryWindow()) { return; } Message msg; while (emitCount < bufferSize && (msg = holdingBuffer.poll()) != null) { processMessage(msg); emitCount++; lastMsg = msg; } } /** * Process jms message. * * @param message */ protected void processMessage(Message message) { try { T payload = convert(message); if (payload != null) { currentWindowRecoveryState.put(message.getJMSMessageID(), payload); emit(payload); } } catch (JMSException e) { throw new RuntimeException("processing msg", e); } } @Override public void handleIdleTime() { Throwable lthrowable = throwable.get(); if (lthrowable == null) { /* nothing to do here, so sleep for a while to avoid busy loop */ try { Thread.sleep(spinMillis); } catch (InterruptedException ie) { throw new RuntimeException(ie); } } else { DTThrowable.rethrow(lthrowable); } } /** * JMS API has a drawback that it only allows acknowledgement/commitment of all the messages which have been consumed * in a session instead of all the messages received till a particular message.
* * This creates complications with recovery/idempotency as we need to ensure that the messages that are being * acknowledged have been persisted because they wouldn't be redelivered. Also if they are persisted then * they shouldn't be re-delivered because that would cause duplicates.
* * This is why when recovery data is persisted and messages are acknowledged, the thread that consumes message is * blocked.
*/ @Override public void endWindow() { if (currentWindowId > windowDataManager.getLargestRecoveryWindow()) { synchronized (lock) { boolean stateSaved = false; boolean ackCompleted = false; try { //No more messages can be consumed now. so we will call emit tuples once more //so that any pending messages can be emitted. Message msg; while ((msg = holdingBuffer.poll()) != null) { processMessage(msg); emitCount++; lastMsg = msg; } windowDataManager.save(currentWindowRecoveryState, context.getId(), currentWindowId); stateSaved = true; currentWindowRecoveryState.clear(); if (lastMsg != null) { acknowledge(); } ackCompleted = true; pendingAck.clear(); } catch (Throwable t) { if (!ackCompleted) { LOG.info("confirm recovery of {} for {} does not exist", context.getId(), currentWindowId, t); } DTThrowable.rethrow(t); } finally { if (stateSaved && !ackCompleted) { try { windowDataManager.delete(context.getId(), currentWindowId); } catch (IOException e) { LOG.error("unable to delete corrupted state", e); } } } } emitCount = 0; //reset emit count } else if (operatorRecoveredWindows != null && currentWindowId < operatorRecoveredWindows[operatorRecoveredWindows.length - 1]) { //pendingAck is not cleared for the last replayed window of this operator. This is because there is //still a chance that in the previous run the operator crashed after saving the state but before acknowledgement. pendingAck.clear(); } context.setCounters(counters); } /** * Commit/Acknowledge messages that have been received.
* @throws javax.jms.JMSException */ protected void acknowledge() throws JMSException { if (isTransacted()) { getSession().commit(); } else if (getSessionAckMode(getAckMode()) == Session.CLIENT_ACKNOWLEDGE) { lastMsg.acknowledge(); // acknowledge all consumed messages till now } } @Override public void checkpointed(long windowId) { } @Override public void committed(long windowId) { try { windowDataManager.deleteUpTo(context.getId(), windowId); } catch (IOException e) { throw new RuntimeException("committing", e); } } @Override public void deactivate() { cleanup(); } @Override protected void cleanup() { try { consumer.setMessageListener(null); replyProducer.close(); replyProducer = null; consumer.close(); consumer = null; super.cleanup(); } catch (JMSException ex) { throw new RuntimeException("at cleanup", ex); } } @Override public void teardown() { windowDataManager.teardown(); } /** * Converts a {@link Message} to type T which is emitted. * * @param message * @return newly constructed tuple from the message. * @throws javax.jms.JMSException */ protected abstract T convert(Message message) throws JMSException; /** * @return the bufferSize */ public int getBufferSize() { return bufferSize; } /** * Sets the number of tuples emitted in each burst. * * @param bufferSize the number of tuples to emit in each burst. */ public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } /** * @return the consumer name */ public String getConsumerName() { return consumerName; } /** * Sets the name for the consumer. * * @param consumerName- the name for the consumer */ public void setConsumerName(String consumerName) { this.consumerName = consumerName; } /** * Sets this idempotent storage manager. * * @param storageManager */ public void setWindowDataManager(WindowDataManager storageManager) { this.windowDataManager = storageManager; } /** * @return the idempotent storage manager. */ public WindowDataManager getWindowDataManager() { return this.windowDataManager; } protected abstract void emit(T payload); public static enum CounterKeys { RECEIVED, REDELIVERED } private static class Lock { } private static final Logger LOG = LoggerFactory.getLogger(AbstractJMSInputOperator.class); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy