/** * 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 org.apache.activemq.transport.amqp.client; import static org.apache.activemq.transport.amqp.AmqpSupport.CONNECTION_OPEN_FAILED; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.transport.InactivityIOException; import org.apache.activemq.transport.amqp.client.sasl.SaslAuthenticator; import org.apache.activemq.transport.amqp.client.transport.NettyTransportListener; import org.apache.activemq.transport.amqp.client.util.AsyncResult; import org.apache.activemq.transport.amqp.client.util.ClientFuture; import org.apache.activemq.transport.amqp.client.util.IdGenerator; import org.apache.activemq.transport.amqp.client.util.NoOpAsyncResult; import org.apache.activemq.transport.amqp.client.util.UnmodifiableProxy; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.engine.Collector; import org.apache.qpid.proton.engine.Connection; import org.apache.qpid.proton.engine.Delivery; import org.apache.qpid.proton.engine.EndpointState; 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.TransportImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; public class AmqpConnection extends AmqpAbstractResource<Connection> implements NettyTransportListener { private static final Logger LOG = LoggerFactory.getLogger(AmqpConnection.class); private static final NoOpAsyncResult NOOP_REQUEST = new NoOpAsyncResult(); private static final int DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 1; // NOTE: Limit default channel max to signed short range to deal with // brokers that don't currently handle the unsigned range well. private static final int DEFAULT_CHANNEL_MAX = 32767; private static final IdGenerator CONNECTION_ID_GENERATOR = new IdGenerator(); public static final long DEFAULT_CONNECT_TIMEOUT = 515000; public static final long DEFAULT_CLOSE_TIMEOUT = 30000; public static final long DEFAULT_DRAIN_TIMEOUT = 60000; private ScheduledThreadPoolExecutor serializer; private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicBoolean connected = new AtomicBoolean(); private final AtomicLong sessionIdGenerator = new AtomicLong(); private final AtomicLong txIdGenerator = new AtomicLong(); private final Collector protonCollector = new CollectorImpl(); private final org.apache.activemq.transport.amqp.client.transport.NettyTransport transport; private final Transport protonTransport = Transport.Factory.create(); private final String username; private final String password; private final URI remoteURI; private final String connectionId; private List<Symbol> desiredCapabilities = Collections.emptyList(); private List<Symbol> offeredCapabilities = Collections.emptyList(); private Map<Symbol, Object> offeredProperties = Collections.emptyMap(); private volatile AmqpFrameValidator sentFrameInspector; private volatile AmqpFrameValidator receivedFrameInspector; private AmqpConnectionListener listener; private SaslAuthenticator authenticator; private String mechanismRestriction; private String authzid; private int idleTimeout = 0; private boolean idleProcessingDisabled; private String containerId; private boolean authenticated; private int channelMax = DEFAULT_CHANNEL_MAX; private long connectTimeout = DEFAULT_CONNECT_TIMEOUT; private long closeTimeout = DEFAULT_CLOSE_TIMEOUT; private long drainTimeout = DEFAULT_DRAIN_TIMEOUT; private boolean trace; private boolean noContainerID = false; public AmqpConnection(org.apache.activemq.transport.amqp.client.transport.NettyTransport transport, String username, String password) { setEndpoint(Connection.Factory.create()); getEndpoint().collect(protonCollector); this.transport = transport; this.username = username; this.password = password; this.connectionId = CONNECTION_ID_GENERATOR.generateId(); this.remoteURI = transport.getRemoteLocation(); this.serializer = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { @Override public Thread newThread(Runnable runner) { Thread serial = new Thread(runner); serial.setDaemon(true); serial.setName(toString()); return serial; } }); // Ensure timely shutdown this.serializer.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); this.serializer.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); this.transport.setTransportListener(this); } public void connect() throws Exception { if (connected.compareAndSet(false, true)) { transport.connect(); final ClientFuture future = new ClientFuture(); serializer.execute(new Runnable() { @Override public void run() { if (!noContainerID) { getEndpoint().setContainer(safeGetContainerId()); } getEndpoint().setHostname(remoteURI.getHost()); if (!getDesiredCapabilities().isEmpty()) { getEndpoint().setDesiredCapabilities(getDesiredCapabilities().toArray(new Symbol[0])); } if (!getOfferedCapabilities().isEmpty()) { getEndpoint().setOfferedCapabilities(getOfferedCapabilities().toArray(new Symbol[0])); } if (!getOfferedProperties().isEmpty()) { getEndpoint().setProperties(getOfferedProperties()); } if (getIdleTimeout() > 0) { protonTransport.setIdleTimeout(getIdleTimeout()); } protonTransport.setMaxFrameSize(getMaxFrameSize()); protonTransport.setChannelMax(getChannelMax()); protonTransport.bind(getEndpoint()); Sasl sasl = protonTransport.sasl(); if (sasl != null) { sasl.client(); } authenticator = new SaslAuthenticator(sasl, username, password, authzid, mechanismRestriction); ((TransportImpl) protonTransport).setProtocolTracer(new AmqpProtocolTracer(AmqpConnection.this)); open(future); pumpToProtonTransport(future); } }); try { if (connectTimeout <= 0) { future.sync(); } else { future.sync(connectTimeout, TimeUnit.MILLISECONDS); if (getEndpoint().getRemoteState() != EndpointState.ACTIVE) { throw new IOException("Failed to connect after configured timeout."); } } } catch (Throwable e) { try { close(); } catch (Throwable ignore) { } throw e; } } } public boolean isConnected() { return transport.isConnected() && connected.get(); } public void close() { if (closed.compareAndSet(false, true)) { final ClientFuture request = new ClientFuture(); 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 (getEndpoint() != null) { close(request); } else { request.onSuccess(); } pumpToProtonTransport(request); } catch (Exception e) { LOG.debug("Caught exception while closing proton connection"); } } }); try { if (closeTimeout <= 0) { request.sync(); } else { request.sync(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()); } } serializer.shutdownNow(); try { if (!serializer.awaitTermination(10, TimeUnit.SECONDS)) { LOG.warn("Serializer didn't shutdown cleanly"); } } catch (InterruptedException e) { } } } } /** * Creates a new Session instance used to create AMQP resources like * senders and receivers. * * @return a new AmqpSession that can be used to create links. * @throws Exception if an error occurs during creation. */ public AmqpSession createSession() throws Exception { checkClosed(); final AmqpSession session = new AmqpSession(AmqpConnection.this, getNextSessionId()); final ClientFuture request = new ClientFuture(); serializer.execute(new Runnable() { @Override public void run() { checkClosed(); session.setEndpoint(getEndpoint().session()); session.setStateInspector(getStateInspector()); session.open(request); pumpToProtonTransport(request); } }); request.sync(); return session; } //----- Access to low level IO for specific test cases -------------------// public void sendRawBytes(final byte[] rawData) throws Exception { checkClosed(); final ClientFuture request = new ClientFuture(); serializer.execute(new Runnable() { @Override public void run() { checkClosed(); try { transport.send(Unpooled.wrappedBuffer(rawData)); } catch (IOException e) { fireClientException(e); } finally { request.onSuccess(); } } }); request.sync(); } //----- Configuration accessors ------------------------------------------// /** * @return the user name that was used to authenticate this connection. */ public String getUsername() { return username; } /** * @return the password that was used to authenticate this connection. */ public String getPassword() { return password; } public void setAuthzid(String authzid) { this.authzid = authzid; } public String getAuthzid() { return authzid; } /** * @return the URI of the remote peer this connection attached to. */ public URI getRemoteURI() { return remoteURI; } /** * @return the container ID that will be set as the container Id. */ public String getContainerId() { return this.containerId; } /** * Sets the container Id that will be configured on the connection prior to * connecting to the remote peer. Calling this after connect has no effect. * * @param containerId the container Id to use on the connection. */ public void setContainerId(String containerId) { this.containerId = containerId; } /** * @return the currently set Max Frame Size value. */ public int getMaxFrameSize() { return DEFAULT_MAX_FRAME_SIZE; } public int getChannelMax() { return channelMax; } public void setChannelMax(int channelMax) { this.channelMax = channelMax; } public long getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(long connectTimeout) { this.connectTimeout = connectTimeout; } public long getCloseTimeout() { return closeTimeout; } public void setCloseTimeout(long closeTimeout) { this.closeTimeout = closeTimeout; } public long getDrainTimeout() { return drainTimeout; } public void setDrainTimeout(long drainTimeout) { this.drainTimeout = drainTimeout; } public List<Symbol> getDesiredCapabilities() { return desiredCapabilities; } public void setDesiredCapabilities(List<Symbol> desiredCapabilities) { if (desiredCapabilities == null) { desiredCapabilities = Collections.emptyList(); } this.desiredCapabilities = desiredCapabilities; } public List<Symbol> getOfferedCapabilities() { return offeredCapabilities; } public void setOfferedCapabilities(List<Symbol> offeredCapabilities) { if (offeredCapabilities == null) { offeredCapabilities = Collections.emptyList(); } this.offeredCapabilities = offeredCapabilities; } public Map<Symbol, Object> getOfferedProperties() { return offeredProperties; } public void setOfferedProperties(Map<Symbol, Object> offeredProperties) { if (offeredProperties == null) { offeredProperties = Collections.emptyMap(); } this.offeredProperties = offeredProperties; } public Connection getConnection() { return UnmodifiableProxy.connectionProxy(getEndpoint()); } public AmqpConnectionListener getListener() { return listener; } public void setListener(AmqpConnectionListener listener) { this.listener = listener; } public int getIdleTimeout() { return idleTimeout; } public void setIdleTimeout(int idleTimeout) { this.idleTimeout = idleTimeout; } public void setIdleProcessingDisabled(boolean value) { this.idleProcessingDisabled = value; } public boolean isIdleProcessingDisabled() { return idleProcessingDisabled; } /** * Sets a restriction on the SASL mechanism to use (if offered by the server). * * @param mechanismRestriction the mechanism to use */ public void setMechanismRestriction(String mechanismRestriction) { this.mechanismRestriction = mechanismRestriction; } public String getMechanismRestriction() { return mechanismRestriction; } public boolean isTraceFrames() { return trace; } public void setTraceFrames(boolean trace) { this.trace = trace; } public AmqpFrameValidator getSentFrameInspector() { return sentFrameInspector; } public void setSentFrameInspector(AmqpFrameValidator amqpFrameInspector) { this.sentFrameInspector = amqpFrameInspector; } public AmqpFrameValidator getReceivedFrameInspector() { return receivedFrameInspector; } public void setReceivedFrameInspector(AmqpFrameValidator amqpFrameInspector) { this.receivedFrameInspector = amqpFrameInspector; } //----- Internal getters used from the child AmqpResource classes --------// ScheduledExecutorService getScheduler() { return this.serializer; } Connection getProtonConnection() { return getEndpoint(); } String getConnectionId() { return this.connectionId; } AmqpTransactionId getNextTransactionId() { return new AmqpTransactionId(connectionId + ":" + txIdGenerator.incrementAndGet()); } void pumpToProtonTransport() { pumpToProtonTransport(NOOP_REQUEST); } void pumpToProtonTransport(AsyncResult request) { try { boolean done = false; while (!done) { ByteBuffer toWrite = protonTransport.getOutputBuffer(); if (toWrite != null && toWrite.hasRemaining()) { ByteBuf outbound = transport.allocateSendBuffer(toWrite.remaining()); outbound.writeBytes(toWrite); transport.send(outbound); protonTransport.outputConsumed(); } else { done = true; } } } catch (IOException e) { fireClientException(e); request.onFailure(e); } } //----- Transport listener event hooks -----------------------------------// @Override public void onData(final ByteBuf incoming) { // We need to retain until the serializer gets around to processing it. ReferenceCountUtil.retain(incoming); serializer.execute(new Runnable() { @Override public void run() { ByteBuffer source = incoming.nioBuffer(); LOG.trace("Client Received from Broker {} bytes:", source.remaining()); if (protonTransport.isClosed()) { LOG.debug("Ignoring incoming data because transport is closed"); return; } 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()); ReferenceCountUtil.release(incoming); // Process the state changes from the latest data and then answer back // any pending updates to the Broker. processUpdates(); pumpToProtonTransport(); } }); } @Override public void onTransportClosed() { LOG.debug("The transport has unexpectedly closed"); failed(getOpenAbortException()); } @Override public void onTransportError(Throwable cause) { fireClientException(cause); } //----- Internal implementation ------------------------------------------// @Override protected void doOpenCompletion() { // If the remote indicates that a close is pending, don't open. if (getEndpoint().getRemoteProperties() == null || !getEndpoint().getRemoteProperties().containsKey(CONNECTION_OPEN_FAILED)) { if (!isIdleProcessingDisabled()) { // Using nano time since it is not related to the wall clock, which may change long initialNow = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); long initialKeepAliveDeadline = protonTransport.tick(initialNow); if (initialKeepAliveDeadline > 0) { getScheduler().schedule(new Runnable() { @Override public void run() { try { if (getEndpoint().getLocalState() != EndpointState.CLOSED) { LOG.debug("Client performing next idle check"); // Using nano time since it is not related to the wall clock, which may change long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); long rescheduleAt = protonTransport.tick(now) - now; pumpToProtonTransport(); if (protonTransport.isClosed()) { LOG.debug("Transport closed after inactivity check."); throw new InactivityIOException("Channel was inactive for to long"); } if (rescheduleAt > 0) { getScheduler().schedule(this, rescheduleAt, TimeUnit.MILLISECONDS); } } } catch (Exception e) { try { transport.close(); } catch (IOException e1) { } fireClientException(e); } } }, initialKeepAliveDeadline - initialNow, TimeUnit.MILLISECONDS); } } super.doOpenCompletion(); } } @Override protected void doOpenInspection() { try { getStateInspector().inspectOpenedResource(getConnection()); } catch (Throwable error) { getStateInspector().markAsInvalid(error.getMessage()); } } @Override protected void doClosedInspection() { try { getStateInspector().inspectClosedResource(getConnection()); } catch (Throwable error) { getStateInspector().markAsInvalid(error.getMessage()); } } protected void fireClientException(Throwable ex) { AmqpConnectionListener listener = this.listener; if (listener != null) { listener.onException(ex); } } protected void checkClosed() throws IllegalStateException { if (closed.get()) { throw new IllegalStateException("The Connection is already closed"); } } private void processUpdates() { try { Event protonEvent = null; while ((protonEvent = protonCollector.peek()) != null) { if (!protonEvent.getType().equals(Type.TRANSPORT)) { LOG.trace("Client: New Proton Event: {}", protonEvent.getType()); } AmqpEventSink amqpEventSink = null; switch (protonEvent.getType()) { case CONNECTION_REMOTE_CLOSE: amqpEventSink = (AmqpEventSink) protonEvent.getConnection().getContext(); amqpEventSink.processRemoteClose(this); break; case CONNECTION_REMOTE_OPEN: amqpEventSink = (AmqpEventSink) protonEvent.getConnection().getContext(); amqpEventSink.processRemoteOpen(this); break; case SESSION_REMOTE_CLOSE: amqpEventSink = (AmqpEventSink) protonEvent.getSession().getContext(); amqpEventSink.processRemoteClose(this); break; case SESSION_REMOTE_OPEN: amqpEventSink = (AmqpEventSink) protonEvent.getSession().getContext(); amqpEventSink.processRemoteOpen(this); break; case LINK_REMOTE_CLOSE: amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext(); amqpEventSink.processRemoteClose(this); break; case LINK_REMOTE_DETACH: amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext(); amqpEventSink.processRemoteDetach(this); break; case LINK_REMOTE_OPEN: amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext(); amqpEventSink.processRemoteOpen(this); break; case LINK_FLOW: amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext(); amqpEventSink.processFlowUpdates(this); break; case DELIVERY: amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext(); amqpEventSink.processDeliveryUpdates(this, (Delivery) protonEvent.getContext()); break; default: break; } protonCollector.pop(); } // We have to do this to pump SASL bytes in as SASL is not event driven yet. if (!authenticated) { processSaslAuthentication(); } } catch (Exception ex) { LOG.warn("Caught Exception during update processing: {}", ex.getMessage(), ex); fireClientException(ex); } } private void processSaslAuthentication() { if (authenticated || authenticator == null) { return; } try { if (authenticator.authenticate()) { authenticator = null; authenticated = true; } } catch (SecurityException ex) { failed(ex); } } private String getNextSessionId() { return connectionId + ":" + sessionIdGenerator.incrementAndGet(); } private String safeGetContainerId() { String containerId = getContainerId(); if (containerId == null || containerId.isEmpty()) { containerId = UUID.randomUUID().toString(); } return containerId; } @Override public String toString() { return "AmqpConnection { " + connectionId + " }"; } public void setNoContainerID() { noContainerID = true; } }