/** * 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 io.hawtjms.provider.amqp; import io.hawtjms.jms.JmsDestination; import io.hawtjms.jms.message.JmsDefaultMessageFactory; import io.hawtjms.jms.message.JmsInboundMessageDispatch; import io.hawtjms.jms.message.JmsMessageFactory; import io.hawtjms.jms.message.JmsOutboundMessageDispatch; import io.hawtjms.jms.meta.*; import io.hawtjms.provider.AbstractAsyncProvider; import io.hawtjms.provider.AsyncResult; import io.hawtjms.provider.ProviderConstants.ACK_TYPE; import io.hawtjms.provider.ProviderRequest; import io.hawtjms.transports.TcpTransport; import io.hawtjms.transports.TransportListener; import io.hawtjms.util.IOExceptionSupport; import io.hawtjms.util.PropertyUtil; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.jms.JMSException; import org.apache.qpid.proton.engine.Collector; import org.apache.qpid.proton.engine.Connection; import org.apache.qpid.proton.engine.EngineFactory; import org.apache.qpid.proton.engine.Event; import org.apache.qpid.proton.engine.Event.Type; import org.apache.qpid.proton.engine.Sasl; import org.apache.qpid.proton.engine.Transport; import org.apache.qpid.proton.engine.impl.CollectorImpl; import org.apache.qpid.proton.engine.impl.EngineFactoryImpl; import org.apache.qpid.proton.engine.impl.ProtocolTracer; import org.apache.qpid.proton.engine.impl.TransportImpl; import org.apache.qpid.proton.framing.TransportFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.vertx.java.core.buffer.Buffer; /** * An AMQP v1.0 Provider. * * The AMQP Provider is bonded to a single remote broker instance. The provider will attempt * to connect to only that instance and once failed can not be recovered. For clients that * wish to implement failover type connections a new AMQP Provider instance must be created * and state replayed from the JMS layer using the standard recovery process defined in the * JMS Provider API. * * All work within this Provider is serialized to a single Thread. Any asynchronous exceptions * will be dispatched from that Thread and all in-bound requests are handled there as well. */ public class AmqpProvider extends AbstractAsyncProvider implements TransportListener { private static final Logger LOG = LoggerFactory.getLogger(AmqpProvider.class); private static final Logger TRACE_BYTES = LoggerFactory.getLogger(AmqpConnection.class.getPackage().getName() + ".BYTES"); private static final Logger TRACE_FRAMES = LoggerFactory.getLogger(AmqpConnection.class.getPackage().getName() + ".FRAMES"); private static final int DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 1; private AmqpConnection connection; private io.hawtjms.transports.Transport transport; private boolean traceFrames; private boolean traceBytes; private boolean presettleConsumers; private boolean presettleProducers; private long connectTimeout = JmsConnectionInfo.DEFAULT_CONNECT_TIMEOUT; private long closeTimeout = JmsConnectionInfo.DEFAULT_CLOSE_TIMEOUT; private long requestTimeout = JmsConnectionInfo.DEFAULT_REQUEST_TIMEOUT; private long sendTimeout = JmsConnectionInfo.DEFAULT_SEND_TIMEOUT; private final JmsDefaultMessageFactory messageFactory = new JmsDefaultMessageFactory(); private final EngineFactory engineFactory = new EngineFactoryImpl(); private final Transport protonTransport = engineFactory.createTransport(); private final Collector protonCollector = new CollectorImpl(); /** * Create a new instance of an AmqpProvider bonded to the given remote URI. * * @param remoteURI * The URI of the AMQP broker this Provider instance will connect to. */ public AmqpProvider(URI remoteURI) { this(remoteURI, null); } /** * Create a new instance of an AmqpProvider bonded to the given remote URI. * * @param remoteURI * The URI of the AMQP broker this Provider instance will connect to. */ public AmqpProvider(URI remoteURI, Map<String, String> extraOptions) { super(remoteURI); updateTracer(); } @Override public void connect() throws IOException { checkClosed(); transport = createTransport(getRemoteURI()); Map<String, String> map = Collections.emptyMap(); try { map = PropertyUtil.parseQuery(remoteURI.getQuery()); } catch (Exception e) { IOExceptionSupport.create(e); } Map<String, String> providerOptions = PropertyUtil.filterProperties(map, "transport."); if (!PropertyUtil.setProperties(transport, providerOptions)) { String msg = "" + " Not all transport options could be set on the AMQP Provider transport." + " Check the options are spelled correctly." + " Given parameters=[" + providerOptions + "]." + " This provider instance cannot be started."; throw new IOException(msg); } transport.connect(); } @Override public void close() { if (closed.compareAndSet(false, true)) { final ProviderRequest<Void> request = new ProviderRequest<Void>(); serializer.execute(new Runnable() { @Override public void run() { try { // If we are not connected then there is nothing we can do now // just signal success. if (!transport.isConnected()) { request.onSuccess(); } if (connection != null) { connection.close(request); } pumpToProtonTransport(); } catch (Exception e) { LOG.debug("Caught exception while closing proton connection"); } } }); try { if (closeTimeout < 0) { request.getResponse(); } else { request.getResponse(closeTimeout, TimeUnit.MILLISECONDS); } } catch (IOException e) { LOG.warn("Error caught while closing Provider: ", e.getMessage()); } finally { if (transport != null) { try { transport.close(); } catch (Exception e) { LOG.debug("Cuaght exception while closing down Transport: {}", e.getMessage()); } } if (serializer != null) { serializer.shutdown(); } } } } @Override public void create(final JmsResource resource, final AsyncResult<Void> request) throws IOException, JMSException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsResourceVistor() { @Override public void processSessionInfo(JmsSessionInfo sessionInfo) throws Exception { AmqpSession session = connection.createSession(sessionInfo); session.open(request); } @Override public void processProducerInfo(JmsProducerInfo producerInfo) throws Exception { AmqpSession session = connection.getSession(producerInfo.getParentId()); AmqpProducer producer = session.createProducer(producerInfo); producer.open(request); } @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { AmqpSession session = connection.getSession(consumerInfo.getParentId()); AmqpConsumer consumer = session.createConsumer(consumerInfo); consumer.open(request); } @Override public void processConnectionInfo(JmsConnectionInfo connectionInfo) throws Exception { closeTimeout = connectionInfo.getCloseTimeout(); connectTimeout = connectionInfo.getConnectTimeout(); sendTimeout = connectionInfo.getSendTimeout(); requestTimeout = connectionInfo.getRequestTimeout(); Connection protonConnection = engineFactory.createConnection(); protonTransport.setMaxFrameSize(getMaxFrameSize()); protonTransport.bind(protonConnection); protonConnection.collect(protonCollector); Sasl sasl = protonTransport.sasl(); if (sasl != null) { sasl.client(); } connection = new AmqpConnection(AmqpProvider.this, protonConnection, sasl, connectionInfo); connection.open(request); } @Override public void processDestination(JmsDestination destination) throws Exception { if (destination.isTemporary()) { AmqpTemporaryDestination temporary = connection.createTemporaryDestination(destination); temporary.open(request); } else { request.onSuccess(); } } @Override public void processTransactionInfo(JmsTransactionInfo transactionInfo) throws Exception { AmqpSession session = connection.getSession(transactionInfo.getParentId()); session.begin(transactionInfo.getTransactionId(), request); } }); pumpToProtonTransport(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void start(final JmsResource resource, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsDefaultResourceVisitor() { @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { AmqpSession session = connection.getSession(consumerInfo.getParentId()); AmqpConsumer consumer = session.getConsumer(consumerInfo); consumer.start(request); } }); pumpToProtonTransport(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void destroy(final JmsResource resource, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsDefaultResourceVisitor() { @Override public void processSessionInfo(JmsSessionInfo sessionInfo) throws Exception { AmqpSession session = connection.getSession(sessionInfo.getSessionId()); session.close(request); } @Override public void processProducerInfo(JmsProducerInfo producerInfo) throws Exception { AmqpSession session = connection.getSession(producerInfo.getParentId()); AmqpProducer producer = session.getProducer(producerInfo); producer.close(request); } @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { AmqpSession session = connection.getSession(consumerInfo.getParentId()); AmqpConsumer consumer = session.getConsumer(consumerInfo); consumer.close(request); } @Override public void processConnectionInfo(JmsConnectionInfo connectionInfo) throws Exception { connection.close(request); } @Override public void processDestination(JmsDestination destination) throws Exception { // TODO - Delete remote temporary Topic or Queue request.onSuccess(null); } }); pumpToProtonTransport(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void send(final JmsOutboundMessageDispatch envelope, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); JmsProducerId producerId = envelope.getProducerId(); AmqpProducer producer = null; if (producerId.getProviderHint() instanceof AmqpFixedProducer) { producer = (AmqpFixedProducer) producerId.getProviderHint(); } else { AmqpSession session = connection.getSession(producerId.getParentId()); producer = session.getProducer(producerId); } boolean couldSend = producer.send(envelope, request); pumpToProtonTransport(); if (couldSend && envelope.isSendAsync()) { request.onSuccess(); } } catch (Exception error) { request.onFailure(error); } } }); } @Override public void acknowledge(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); AmqpSession amqpSession = connection.getSession(sessionId); amqpSession.acknowledge(); pumpToProtonTransport(); request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void acknowledge(final JmsInboundMessageDispatch envelope, final ACK_TYPE ackType, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); JmsConsumerId consumerId = envelope.getConsumerId(); AmqpConsumer consumer = null; if (consumerId.getProviderHint() instanceof AmqpConsumer) { consumer = (AmqpConsumer) consumerId.getProviderHint(); } else { AmqpSession session = connection.getSession(consumerId.getParentId()); consumer = session.getConsumer(consumerId); } consumer.acknowledge(envelope, ackType); if (consumer.getSession().isAsyncAck()) { request.onSuccess(); pumpToProtonTransport(); } else { pumpToProtonTransport(); request.onSuccess(); } } catch (Exception error) { request.onFailure(error); } } }); } @Override public void commit(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); AmqpSession session = connection.getSession(sessionId); session.commit(request); pumpToProtonTransport(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void rollback(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); AmqpSession session = connection.getSession(sessionId); session.rollback(request); pumpToProtonTransport(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void recover(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); AmqpSession session = connection.getSession(sessionId); session.recover(); pumpToProtonTransport(); request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void unsubscribe(final String subscription, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); pumpToProtonTransport(); request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void pull(final JmsConsumerId consumerId, final long timeout, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); AmqpConsumer consumer = null; if (consumerId.getProviderHint() instanceof AmqpConsumer) { consumer = (AmqpConsumer) consumerId.getProviderHint(); } else { AmqpSession session = connection.getSession(consumerId.getParentId()); consumer = session.getConsumer(consumerId); } consumer.pull(timeout); pumpToProtonTransport(); request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } /** * Provides an extension point for subclasses to insert other types of transports such * as SSL etc. * * @param remoteLocation * The remote location where the transport should attempt to connect. * * @return the newly created transport instance. */ protected io.hawtjms.transports.Transport createTransport(URI remoteLocation) { return new TcpTransport(this, remoteLocation); } private void updateTracer() { if (isTraceFrames()) { ((TransportImpl) protonTransport).setProtocolTracer(new ProtocolTracer() { @Override public void receivedFrame(TransportFrame transportFrame) { TRACE_FRAMES.trace("RECV: {}", transportFrame.getBody()); } @Override public void sentFrame(TransportFrame transportFrame) { TRACE_FRAMES.trace("SENT: {}", transportFrame.getBody()); } }); } } @Override public void onData(Buffer input) { // Create our own copy since we will process later. final ByteBuffer source = ByteBuffer.wrap(input.getBytes()); serializer.execute(new Runnable() { @Override public void run() { LOG.trace("Received from Broker {} bytes:", source.remaining()); do { ByteBuffer buffer = protonTransport.getInputBuffer(); int limit = Math.min(buffer.remaining(), source.remaining()); ByteBuffer duplicate = source.duplicate(); duplicate.limit(source.position() + limit); buffer.put(duplicate); protonTransport.processInput(); source.position(source.position() + limit); } while (source.hasRemaining()); // Process the state changes from the latest data and then answer back // any pending updates to the Broker. processUpdates(); pumpToProtonTransport(); } }); } /** * Callback method for the Transport to report connection errors. When called * the method will queue a new task to fire the failure error back to the listener. * * @param error * the error that causes the transport to fail. */ @Override public void onTransportError(final Throwable error) { if (!closed.get()) { serializer.execute(new Runnable() { @Override public void run() { LOG.info("Transport failed: {}", error.getMessage()); if (!closed.get()) { fireProviderException(error); } } }); } } /** * Callback method for the Transport to report that the underlying connection * has closed. When called this method will queue a new task that will check for * the closed state on this transport and if not closed then an exception is raied * to the registered ProviderListener to indicate connection loss. */ @Override public void onTransportClosed() { if (!closed.get()) { serializer.execute(new Runnable() { @Override public void run() { LOG.debug("Transport connection remotely closed:"); if (!closed.get()) { fireProviderException(new IOException("Connection remotely closed.")); } } }); } } private void processUpdates() { try { Event protonEvent = null; while ((protonEvent = protonCollector.peek()) != null) { if (!protonEvent.getType().equals(Type.TRANSPORT)) { LOG.trace("New Proton Event: {}", protonEvent.getType()); } AmqpResource amqpResource = null; switch (protonEvent.getType()) { case CONNECTION_REMOTE_STATE: AmqpConnection connection = (AmqpConnection) protonEvent.getConnection().getContext(); connection.processStateChange(); break; case SESSION_REMOTE_STATE: AmqpSession session = (AmqpSession) protonEvent.getSession().getContext(); session.processStateChange(); break; case LINK_REMOTE_STATE: AmqpResource resource = (AmqpResource) protonEvent.getLink().getContext(); resource.processStateChange(); break; case LINK_FLOW: amqpResource = (AmqpResource) protonEvent.getLink().getContext(); amqpResource.processFlowUpdates(); break; case DELIVERY: amqpResource = (AmqpResource) protonEvent.getLink().getContext(); amqpResource.processDeliveryUpdates(); break; default: break; } protonCollector.pop(); } connection.processUpdates(); } catch (Exception ex) { LOG.warn("Caught Exception during update processing: {}", ex.getMessage()); fireProviderException(ex); } } private void pumpToProtonTransport() { try { boolean done = false; while (!done) { ByteBuffer toWrite = protonTransport.getOutputBuffer(); if (toWrite != null && toWrite.hasRemaining()) { // TODO - Get Bytes in a readable form if (isTraceBytes()) { TRACE_BYTES.info("Sending: {}", toWrite.toString()); } transport.send(toWrite); protonTransport.outputConsumed(); } else { done = true; } } } catch (IOException e) { fireProviderException(e); } } //---------- Property Setters and Getters --------------------------------// @Override public JmsMessageFactory getMessageFactory() { return this.messageFactory; } public void setTraceFrames(boolean trace) { this.traceFrames = trace; } public boolean isTraceFrames() { return this.traceFrames; } public void setTraceBytes(boolean trace) { this.traceBytes = trace; } public boolean isTraceBytes() { return this.traceBytes; } public long getCloseTimeout() { return this.closeTimeout; } public void setCloseTimeout(long closeTimeout) { this.closeTimeout = closeTimeout; } public long getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(long connectTimeout) { this.connectTimeout = connectTimeout; } public long getRequestTimeout() { return requestTimeout; } public void setRequestTimeout(long requestTimeout) { this.requestTimeout = requestTimeout; } public long getSendTimeout() { return sendTimeout; } public void setSendTimeout(long sendTimeout) { this.sendTimeout = sendTimeout; } public void setPresettle(boolean presettle) { setPresettleConsumers(presettle); setPresettleProducers(presettle); } public boolean isPresettleConsumers() { return this.presettleConsumers; } public void setPresettleConsumers(boolean presettle) { this.presettleConsumers = presettle; } public boolean isPresettleProducers() { return this.presettleProducers; } public void setPresettleProducers(boolean presettle) { this.presettleProducers = presettle; } /** * @return the currently set Max Frame Size value. */ public int getMaxFrameSize() { return DEFAULT_MAX_FRAME_SIZE; } @Override public String toString() { return "AmqpProvider: " + getRemoteURI().getHost() + ":" + getRemoteURI().getPort(); } }