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

com.rabbitmq.client.amqp.impl.AmqpConnection Maven / Gradle / Ivy

Go to download

The RabbitMQ AMQP 1.0 Java client library defines an API to access RabbitMQ with the AMQP 1.0 protocol.

There is a newer version: 0.1.0
Show newest version
// Copyright (c) 2024 Broadcom. All Rights Reserved.
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
//
// Licensed 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.
//
// If you have any questions regarding licensing, please contact us at
// [email protected].
package com.rabbitmq.client.amqp.impl;

import static com.rabbitmq.client.amqp.Resource.State.*;

import com.rabbitmq.client.amqp.*;
import com.rabbitmq.client.amqp.ObservationCollector;
import com.rabbitmq.client.amqp.impl.Utils.StopWatch;
import com.rabbitmq.client.amqp.metrics.MetricsCollector;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.qpid.protonj2.client.ConnectionOptions;
import org.apache.qpid.protonj2.client.DisconnectionEvent;
import org.apache.qpid.protonj2.client.Session;
import org.apache.qpid.protonj2.client.SslOptions;
import org.apache.qpid.protonj2.client.exceptions.ClientException;
import org.apache.qpid.protonj2.client.exceptions.ClientIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class AmqpConnection extends ResourceBase implements Connection {

  private static final Predicate RECOVERY_PREDICATE =
      t -> t instanceof AmqpException.AmqpConnectionException;

  private static final AtomicLong ID_SEQUENCE = new AtomicLong(0);

  private static final Logger LOGGER = LoggerFactory.getLogger(AmqpConnection.class);
  private final long id;
  private final AmqpEnvironment environment;
  private final AmqpManagement management;
  private volatile org.apache.qpid.protonj2.client.Connection nativeConnection;
  private volatile Address connectionAddress;
  private volatile String connectionNodename;
  private final AtomicBoolean closed = new AtomicBoolean(false);
  private volatile Session nativeSession;
  private final List publishers = new CopyOnWriteArrayList<>();
  private final List consumers = new CopyOnWriteArrayList<>();
  private final List rpcClients = new CopyOnWriteArrayList<>();
  private final List rpcServers = new CopyOnWriteArrayList<>();
  private final TopologyListener topologyListener;
  private volatile EntityRecovery entityRecovery;
  private final AtomicBoolean recoveringConnection = new AtomicBoolean(false);
  private final DefaultConnectionSettings connectionSettings;
  private final Supplier sessionHandlerSupplier;
  private final ConnectionUtils.AffinityContext affinity;
  private final ConnectionSettings.AffinityStrategy affinityStrategy;
  private final String name;
  private final Lock instanceLock = new ReentrantLock();

  AmqpConnection(AmqpConnectionBuilder builder) {
    super(builder.listeners());
    this.id = ID_SEQUENCE.getAndIncrement();
    this.name = builder.name();
    this.environment = builder.environment();
    this.connectionSettings = builder.connectionSettings().consolidate();
    this.sessionHandlerSupplier =
        builder.isolateResources()
            ? () -> new SessionHandler.SingleSessionSessionHandler(this)
            : () -> new SessionHandler.ConnectionNativeSessionSessionHandler(this);
    BiConsumer disconnectHandler;
    AmqpConnectionBuilder.AmqpRecoveryConfiguration recoveryConfiguration =
        builder.recoveryConfiguration();

    this.topologyListener = createTopologyListener(builder);

    if (recoveryConfiguration.activated()) {
      disconnectHandler = recoveryDisconnectHandler(recoveryConfiguration, builder.name());
    } else {
      disconnectHandler =
          (c, e) -> {
            AmqpException failureCause =
                ExceptionUtils.convert(e.failureCause(), "Connection disconnected");
            this.close(failureCause);
          };
    }
    if (this.connectionSettings.affinity().activated()) {
      this.affinity =
          new ConnectionUtils.AffinityContext(
              this.connectionSettings.affinity().queue(),
              this.connectionSettings.affinity().operation());
      this.affinityStrategy = connectionSettings.affinity().strategy();
    } else {
      this.affinity = null;
      this.affinityStrategy = null;
    }
    this.management = createManagement();
    LOGGER.debug("Opening native connection for connection '{}'...", this.name());
    NativeConnectionWrapper ncw =
        ConnectionUtils.enforceAffinity(
            addrs -> {
              NativeConnectionWrapper wrapper =
                  connect(this.connectionSettings, builder.name(), disconnectHandler, addrs);
              this.nativeConnection = wrapper.connection();
              return wrapper;
            },
            this.management,
            this.affinity,
            this.environment.affinityCache(),
            this.affinityStrategy,
            ConnectionUtils.NO_RETRY_STRATEGY,
            this.name());
    this.sync(ncw);
    LOGGER.debug("Opened connection '{}' on node '{}'.", this.name(), this.connectionNodename());
    this.state(OPEN);
    this.environment.metricsCollector().openConnection();
  }

  @Override
  public Management management() {
    checkOpen();
    return this.managementNoCheck();
  }

  Management managementNoCheck() {
    this.management.init();
    return this.management;
  }

  AmqpManagement createManagement() {
    return new AmqpManagement(
        new AmqpManagementParameters(this).topologyListener(this.topologyListener));
  }

  @Override
  public PublisherBuilder publisherBuilder() {
    checkOpen();
    return new AmqpPublisherBuilder(this);
  }

  @Override
  public ConsumerBuilder consumerBuilder() {
    checkOpen();
    return new AmqpConsumerBuilder(this);
  }

  @Override
  public RpcClientBuilder rpcClientBuilder() {
    return new RpcSupport.AmqpRpcClientBuilder(this);
  }

  @Override
  public RpcServerBuilder rpcServerBuilder() {
    return new RpcSupport.AmqpRpcServerBuilder(this);
  }

  @Override
  public void close() {
    this.close(null);
  }

  // internal API

  private NativeConnectionWrapper connect(
      DefaultConnectionSettings connectionSettings,
      String name,
      BiConsumer disconnectHandler,
      List
addresses) { ConnectionOptions connectionOptions = new ConnectionOptions(); if (connectionSettings.credentialsProvider() instanceof UsernamePasswordCredentialsProvider) { UsernamePasswordCredentialsProvider credentialsProvider = (UsernamePasswordCredentialsProvider) connectionSettings.credentialsProvider(); connectionOptions.user(credentialsProvider.getUsername()); connectionOptions.password(credentialsProvider.getPassword()); } connectionOptions.virtualHost("vhost:" + connectionSettings.virtualHost()); connectionOptions.saslOptions().addAllowedMechanism(connectionSettings.saslMechanism()); connectionOptions.idleTimeout( connectionSettings.idleTimeout().toMillis(), TimeUnit.MILLISECONDS); connectionOptions.disconnectedHandler(disconnectHandler); if (name == null) { connectionOptions.properties(ClientProperties.DEFAULT_CLIENT_PROPERTIES); } else { Map props = new LinkedHashMap<>(ClientProperties.DEFAULT_CLIENT_PROPERTIES); props.put("connection_name", name); connectionOptions.properties(Map.copyOf(props)); } if (connectionSettings.tlsEnabled()) { DefaultConnectionSettings.DefaultTlsSettings tlsSettings = connectionSettings.tlsSettings(); connectionOptions.sslEnabled(true); SslOptions sslOptions = connectionOptions.sslOptions(); sslOptions.sslContextOverride(tlsSettings.sslContext()); sslOptions.verifyHost(tlsSettings.isHostnameVerification()); } Address address = connectionSettings.selectAddress(addresses); StopWatch stopWatch = new StopWatch(); try { LOGGER.trace("Connecting '{}' to {}...", this.name(), address); org.apache.qpid.protonj2.client.Connection connection = this.environment.client().connect(address.host(), address.port(), connectionOptions); LOGGER.debug("Created native connection instance for '{}'", this.name()); ExceptionUtils.wrapGet(connection.openFuture()); LOGGER.debug("Connection attempt '{}' succeeded", this.name()); checkBrokerVersion(connection); return new NativeConnectionWrapper(connection, extractNode(connection), address); } catch (ClientException e) { throw ExceptionUtils.convert(e); } finally { LOGGER.debug("Connection attempt for '{}' took {}", this.name(), stopWatch.stop()); } } private void sync(NativeConnectionWrapper wrapper) { this.connectionAddress = wrapper.address(); this.connectionNodename = wrapper.nodename(); this.nativeConnection = wrapper.connection(); } private static void checkBrokerVersion(org.apache.qpid.protonj2.client.Connection connection) throws ClientException { String version = (String) connection.properties().get("version"); if (version == null) { throw new AmqpException("No broker version set in connection properties"); } if (!Utils.is4_0_OrMore(version)) { throw new AmqpException("The AMQP client library requires RabbitMQ 4.0 or more"); } } private static String brokerVersion(org.apache.qpid.protonj2.client.Connection connection) { try { return (String) connection.properties().get("version"); } catch (ClientException e) { throw ExceptionUtils.convert(e); } } String brokerVersion() { return brokerVersion(this.nativeConnection); } private static String extractNode(org.apache.qpid.protonj2.client.Connection connection) throws ClientException { String node = (String) connection.properties().get("node"); if (node == null) { throw new AmqpException("The broker node name is not available"); } return node; } TopologyListener createTopologyListener(AmqpConnectionBuilder builder) { TopologyListener topologyListener; if (builder.recoveryConfiguration().topology()) { RecordingTopologyListener rtl = new RecordingTopologyListener( "topology-listener-connection-" + this.name(), this.environment.recoveryEventLoop()); this.entityRecovery = new EntityRecovery(this, rtl); topologyListener = rtl; } else { topologyListener = TopologyListener.NO_OP; } return builder.topologyListener() == null ? topologyListener : TopologyListener.compose(List.of(builder.topologyListener(), topologyListener)); } private BiConsumer recoveryDisconnectHandler( AmqpConnectionBuilder.AmqpRecoveryConfiguration recoveryConfiguration, String name) { AtomicReference> resultReference = new AtomicReference<>(); BiConsumer result = (conn, event) -> { ClientIOException ioex = event.failureCause(); LOGGER.debug( "Disconnect handler of '{}', error is the following: {}", this.name(), ioex.getMessage()); if (this.state() == OPENING) { LOGGER.debug("Connection is still opening, disconnect handler skipped"); // the broker is not available when opening the connection // nothing to do in this listener return; } if (this.recoveringConnection.get()) { LOGGER.debug( "Filtering recovery task scheduling, connection recovery of '{}' already in progress", this.name()); return; } AmqpException exception = ExceptionUtils.convert(event.failureCause()); LOGGER.debug("Converted native exception to {}", exception.getClass().getSimpleName()); if (RECOVERY_PREDICATE.test(exception) && this.state() != OPENING) { LOGGER.debug( "Queueing recovery task for '{}', error is {}", this.name(), exception.getMessage()); this.environment .executorService() .submit( () -> { if (!this.recoveringConnection.get()) { recoverAfterConnectionFailure( recoveryConfiguration, name, exception, resultReference); } }); } else { LOGGER.debug( "Not recovering connection '{}' for error {}", this.name(), event.failureCause().getMessage()); } }; resultReference.set(result); return result; } private void recoverAfterConnectionFailure( AmqpConnectionBuilder.AmqpRecoveryConfiguration recoveryConfiguration, String connectionName, Exception failureCause, AtomicReference> disconnectedHandlerReference) { LOGGER.info( "Connection '{}' to '{}' has been disconnected, trying to recover.", this.name(), this.currentConnectionLabel()); LOGGER.debug("Notifying listeners of connection '{}'.", this.name()); this.state(RECOVERING, failureCause); this.changeStateOfPublishers(RECOVERING, failureCause); this.changeStateOfConsumers(RECOVERING, failureCause); this.nativeConnection = null; this.nativeSession = null; this.connectionAddress = null; LOGGER.debug("Releasing management resource of connection '{}'.", this.name()); this.releaseManagementResources(); CompletableFuture ncwFuture; if (this.recoveringConnection.compareAndSet(false, true)) { this.recoveringConnection.set(true); LOGGER.debug("Connection attempt for '{}'.", this.name()); ncwFuture = recoverNativeConnection( recoveryConfiguration, connectionName, disconnectedHandlerReference); } else { LOGGER.debug("Connection '{}' already recovering, returning.", this.name()); return; } ncwFuture .thenAccept( ncw -> { this.sync(ncw); LOGGER.debug("Reconnected '{}' to {}", this.name(), this.currentConnectionLabel()); this.recoveringConnection.set(false); try { if (recoveryConfiguration.topology()) { this.management.init(); LOGGER.debug("Recovering topology of connection '{}'...", this.name()); this.recoverTopology(); this.recoverConsumers(); this.recoverPublishers(); LOGGER.debug("Recovered topology of connection '{}'.", this.name()); } LOGGER.info( "Recovered connection '{}' to {}", this.name(), this.currentConnectionLabel()); this.state(OPEN); } catch (Exception ex) { // likely InterruptedException or IO exception LOGGER.info( "Error while trying to recover topology for connection '{}': {}", this.name(), ex.getMessage()); if (RECOVERY_PREDICATE.test(ex)) { LOGGER.debug( "Error during topology recoverable, queueing recovery task for '{}', error is {}", this.name(), ex.getMessage()); this.environment .executorService() .submit( () -> { if (!this.recoveringConnection.get()) { recoverAfterConnectionFailure( recoveryConfiguration, name, ex, disconnectedHandlerReference); } }); } } }) .exceptionally( t -> { this.recoveringConnection.set(false); if (t instanceof InterruptedException) { Thread.currentThread().interrupt(); } this.close(t); return null; }); } private CompletableFuture recoverNativeConnection( AmqpConnectionBuilder.AmqpRecoveryConfiguration recoveryConfiguration, String connectionName, AtomicReference> disconnectedHandlerReference) { return AsyncRetry.asyncRetry( () -> ConnectionUtils.enforceAffinity( addrs -> { NativeConnectionWrapper wrapper = connect( this.connectionSettings, connectionName, disconnectedHandlerReference.get(), addrs); this.nativeConnection = wrapper.connection(); return wrapper; }, this.management, this.affinity, this.environment.affinityCache(), this.affinityStrategy, new ConnectionUtils.RetryStrategy() { @Override public T maybeRetry(Supplier task) { return RetryUtils.callAndMaybeRetry( task::get, e -> true, Duration.ofMillis(10), 5, "Connection affinity operation"); } }, connectionName)) .description("Trying to create native connection for '%s'.", connectionName) .delayPolicy(recoveryConfiguration.backOffDelayPolicy()) .retry(RECOVERY_PREDICATE) .scheduler(this.scheduledExecutorService()) .build(); } private void recoverTopology() throws InterruptedException { if (this.entityRecovery != null) { Utils.throwIfInterrupted(); this.entityRecovery.recover(); } } private void recoverConsumers() throws InterruptedException { if (this.consumers.isEmpty()) { LOGGER.debug("No consumers to recover"); } else { LOGGER.debug("{} consumer(s) to recover", this.consumers.size()); List failedConsumers = new ArrayList<>(); for (AmqpConsumer consumer : this.consumers) { Utils.throwIfInterrupted(); try { LOGGER.debug("Recovering consumer {} (queue '{}')", consumer.id(), consumer.queue()); consumer.recoverAfterConnectionFailure(); consumer.state(OPEN); LOGGER.debug("Recovered consumer {} (queue '{}')", consumer.id(), consumer.queue()); } catch (Exception ex) { LOGGER.warn( "Error while trying to recover consumer {} (queue '{}')", consumer.id(), consumer.queue(), ex); failedConsumers.add(consumer); } } failedConsumers.forEach(AmqpConsumer::close); } } private void recoverPublishers() throws InterruptedException { if (this.publishers.isEmpty()) { LOGGER.debug("No publishers to recover"); } else { LOGGER.debug("{} publisher(s) to recover", this.publishers.size()); List failedPublishers = new ArrayList<>(); for (AmqpPublisher publisher : this.publishers) { Utils.throwIfInterrupted(); try { LOGGER.debug( "Recovering publisher {} (address '{}')", publisher.id(), publisher.address()); publisher.recoverAfterConnectionFailure(); publisher.state(OPEN); LOGGER.debug( "Recovered publisher {} (address '{}')", publisher.id(), publisher.address()); } catch (Exception ex) { LOGGER.warn( "Error while trying to recover publisher {} (address '{}')", publisher.id(), publisher.address(), ex); failedPublishers.add(publisher); } } failedPublishers.forEach(AmqpPublisher::close); } } private void closeManagement() { this.management.close(); } private void releaseManagementResources() { if (this.management != null) { this.management.releaseResources(); } } Session nativeSession() { return nativeSession(true); } Session nativeSession(boolean check) { if (check) { checkOpen(); } Session result = this.nativeSession; if (result == null) { this.instanceLock.lock(); try { result = this.nativeSession; if (result == null) { if (check) { checkOpen(); } this.nativeSession = result = this.openSession(this.nativeConnection); } } finally { this.instanceLock.unlock(); } } return result; } private Session openSession(org.apache.qpid.protonj2.client.Connection connection) { try { return connection.openSession(); } catch (ClientException e) { throw ExceptionUtils.convert(e, "Error while opening session"); } } org.apache.qpid.protonj2.client.Connection nativeConnection() { return this.nativeConnection; } AmqpEnvironment environment() { return this.environment; } ScheduledExecutorService scheduledExecutorService() { return this.environment.scheduledExecutorService(); } Clock clock() { return this.environment.clock(); } MetricsCollector metricsCollector() { return this.environment.metricsCollector(); } ObservationCollector observationCollector() { return this.environment.observationCollector(); } SessionHandler createSessionHandler() { return this.sessionHandlerSupplier.get(); } Publisher createPublisher(AmqpPublisherBuilder builder) { // TODO copy the builder properties to create the publisher AmqpPublisher publisher = new AmqpPublisher(builder); this.publishers.add(publisher); return publisher; } void removePublisher(AmqpPublisher publisher) { this.publishers.remove(publisher); } Consumer createConsumer(AmqpConsumerBuilder builder) { // TODO copy the builder properties to create the consumer AmqpConsumer consumer = new AmqpConsumer(builder); this.consumers.add(consumer); this.topologyListener.consumerCreated(consumer.id(), builder.queue()); return consumer; } void removeConsumer(AmqpConsumer consumer) { this.consumers.remove(consumer); this.topologyListener.consumerDeleted(consumer.id(), consumer.queue()); } RpcClient createRpcClient(RpcSupport.AmqpRpcClientBuilder builder) { RpcClient rpcClient = new AmqpRpcClient(builder); this.rpcClients.add(rpcClient); return rpcClient; } void removeRpcClient(RpcClient rpcClient) { this.rpcClients.remove(rpcClient); } RpcServer createRpcServer(RpcSupport.AmqpRpcServerBuilder builder) { RpcServer rpcServer = new AmqpRpcServer(builder); this.rpcServers.add(rpcServer); return rpcServer; } void removeRpcServer(RpcServer rpcServer) { this.rpcServers.remove(rpcServer); } private void changeStateOfPublishers(State newState, Throwable failure) { this.changeStateOfResources(this.publishers, newState, failure); } private void changeStateOfConsumers(State newState, Throwable failure) { this.changeStateOfResources(this.consumers, newState, failure); } private void changeStateOfResources( List resources, State newState, Throwable failure) { resources.forEach(r -> r.state(newState, failure)); } private String currentConnectionLabel() { if (this.connectionAddress == null) { return ""; } else { return this.connectionAddress.host() + ":" + this.connectionAddress.port(); } } Address connectionAddress() { return this.connectionAddress; } String connectionNodename() { return this.connectionNodename; } String name() { return this.name == null ? "" : this.name; } ConnectionUtils.AffinityContext affinity() { return this.affinity; } long id() { return this.id; } private void close(Throwable cause) { if (this.closed.compareAndSet(false, true)) { this.state(CLOSING, cause); this.environment.removeConnection(this); if (this.topologyListener instanceof AutoCloseable) { try { ((AutoCloseable) this.topologyListener).close(); } catch (Exception e) { LOGGER.info("Error while closing topology listener", e); } } this.closeManagement(); for (RpcClient rpcClient : this.rpcClients) { rpcClient.close(); } for (RpcServer rpcServer : this.rpcServers) { rpcServer.close(); } for (AmqpPublisher publisher : this.publishers) { publisher.close(); } for (AmqpConsumer consumer : this.consumers) { consumer.close(); } try { org.apache.qpid.protonj2.client.Connection nc = this.nativeConnection; if (nc != null) { nc.close(); } } catch (Exception e) { LOGGER.warn("Error while closing native connection", e); } this.state(CLOSED, cause); this.environment.metricsCollector().closeConnection(); } } @Override public String toString() { return this.environment.toString() + "-" + this.id; } static class NativeConnectionWrapper { private final org.apache.qpid.protonj2.client.Connection connection; private final String nodename; private final Address address; NativeConnectionWrapper( org.apache.qpid.protonj2.client.Connection connection, String nodename, Address address) { this.connection = connection; this.nodename = nodename; this.address = address; } String nodename() { return this.nodename; } Address address() { return this.address; } org.apache.qpid.protonj2.client.Connection connection() { return this.connection; } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AmqpConnection that = (AmqpConnection) o; return id == that.id; } @Override public int hashCode() { return Objects.hashCode(id); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy