org.eclipse.ditto.services.connectivity.messaging.rabbitmq.RabbitMQClientActor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ditto.services.connectivity.messaging.rabbitmq.RabbitMQClientActor.java

Source

/*
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.ditto.services.connectivity.messaging.rabbitmq;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import javax.annotation.Nullable;

import org.eclipse.ditto.model.base.auth.AuthorizationContext;
import org.eclipse.ditto.model.connectivity.Connection;
import org.eclipse.ditto.model.connectivity.ConnectivityModelFactory;
import org.eclipse.ditto.model.connectivity.ConnectivityStatus;
import org.eclipse.ditto.model.connectivity.Enforcement;
import org.eclipse.ditto.model.connectivity.HeaderMapping;
import org.eclipse.ditto.model.connectivity.Target;
import org.eclipse.ditto.services.connectivity.messaging.BaseClientActor;
import org.eclipse.ditto.services.connectivity.messaging.BaseClientData;
import org.eclipse.ditto.services.connectivity.messaging.BaseClientState;
import org.eclipse.ditto.services.connectivity.messaging.internal.ClientConnected;
import org.eclipse.ditto.services.connectivity.messaging.internal.ClientDisconnected;
import org.eclipse.ditto.services.connectivity.messaging.internal.ImmutableConnectionFailure;
import org.eclipse.ditto.services.connectivity.util.ConnectionLogUtil;
import org.eclipse.ditto.services.utils.config.InstanceIdentifierSupplier;
import org.eclipse.ditto.signals.commands.connectivity.exceptions.ConnectionFailedException;

import com.newmotion.akka.rabbitmq.ChannelActor;
import com.newmotion.akka.rabbitmq.ChannelCreated;
import com.newmotion.akka.rabbitmq.CreateChannel;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AlreadyClosedException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Delivery;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.impl.DefaultExceptionHandler;

import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.Scheduler;
import akka.actor.Status;
import akka.japi.pf.FI;
import akka.japi.pf.FSMStateFunctionBuilder;
import akka.pattern.Patterns;
import scala.Option;
import scala.concurrent.ExecutionContext;
import scala.concurrent.duration.FiniteDuration;

/**
 * Actor which handles connection to AMQP 0.9.1 server.
 */
public final class RabbitMQClientActor extends BaseClientActor {

    private static final String RMQ_CONNECTION_ACTOR_NAME = "rmq-connection";
    private static final String CONSUMER_CHANNEL = "consumer-channel";
    private static final String PUBLISHER_CHANNEL = "publisher-channel";
    private static final String CONSUMER_ACTOR_PREFIX = "consumer-";

    private final RabbitConnectionFactoryFactory rabbitConnectionFactoryFactory;
    private final Map<String, String> consumedTagsToAddresses;
    private final Map<String, ActorRef> consumerByAddressWithIndex;

    @Nullable
    private ActorRef rmqConnectionActor;
    private ActorRef rmqPublisherActor;

    /*
     * This constructor is called via reflection by the static method propsForTest.
     */
    @SuppressWarnings("unused")
    private RabbitMQClientActor(final Connection connection, final ConnectivityStatus connectionStatus,
            final RabbitConnectionFactoryFactory rabbitConnectionFactoryFactory,
            final ActorRef conciergeForwarder) {

        super(connection, connectionStatus, conciergeForwarder);

        this.rabbitConnectionFactoryFactory = rabbitConnectionFactoryFactory;
        consumedTagsToAddresses = new HashMap<>();
        consumerByAddressWithIndex = new HashMap<>();

        rmqConnectionActor = null;
    }

    /*
     * This constructor is called via reflection by the static method props(Connection, ActorRef).
     */
    @SuppressWarnings("unused")
    private RabbitMQClientActor(final Connection connection, final ConnectivityStatus connectionStatus,
            final ActorRef conciergeForwarder) {

        this(connection, connectionStatus, ConnectionBasedRabbitConnectionFactoryFactory.getInstance(),
                conciergeForwarder);
    }

    /**
     * Creates Akka configuration object for this actor.
     *
     * @param connection the connection.
     * @param conciergeForwarder the actor used to send signals to the concierge service.
     * @return the Akka configuration Props object.
     */
    public static Props props(final Connection connection, final ActorRef conciergeForwarder) {

        return Props.create(RabbitMQClientActor.class, validateConnection(connection),
                connection.getConnectionStatus(), conciergeForwarder);
    }

    /**
     * Creates Akka configuration object for this actor.
     *
     * @param connection the connection.
     * @param connectionStatus the desired status of the.
     * @param conciergeForwarder the actor used to send signals to the concierge service.
     * @param rabbitConnectionFactoryFactory the ConnectionFactory Factory to use.
     * @return the Akka configuration Props object.
     */
    static Props propsForTests(final Connection connection, final ConnectivityStatus connectionStatus,
            final ActorRef conciergeForwarder,
            final RabbitConnectionFactoryFactory rabbitConnectionFactoryFactory) {

        return Props.create(RabbitMQClientActor.class, validateConnection(connection), connectionStatus,
                rabbitConnectionFactoryFactory, conciergeForwarder);
    }

    private static Connection validateConnection(final Connection connection) {
        // the target addresses must have the format exchange/routingKey for RabbitMQ
        connection.getTargets().stream().map(Target::getAddress).forEach(RabbitMQTarget::fromTargetAddress);
        return connection;
    }

    @Override
    protected FSMStateFunctionBuilder<BaseClientState, BaseClientData> inConnectedState() {
        return super.inConnectedState().event(ClientConnected.class, BaseClientData.class, (event, data) -> {
            // when connection is lost, the library (ChannelActor) will automatically reconnect
            // without the state of this actor changing. But we will receive a new ClientConnected message
            // that we can use to bind our consumers to the channels.
            allocateResourcesOnConnection(event);
            return stay();
        });
    }

    @Override
    protected CompletionStage<Status.Status> doTestConnection(final Connection connection) {
        // should be smaller than the global testing timeout to be able to send a response
        final Duration createChannelTimeout = clientConfig.getTestingTimeout().dividedBy(10L).multipliedBy(8L);
        final Duration internalReconnectTimeout = clientConfig.getTestingTimeout();
        // does explicitly not test the consumer so we won't consume any messages by accident.
        return connect(connection, createChannelTimeout, internalReconnectTimeout);
    }

    @Override
    protected void doConnectClient(final Connection connection, @Nullable final ActorRef origin) {
        final boolean consuming = isConsuming();
        final ActorRef self = getSelf();
        // #connect() will only create the channel for the the producer, but not the consumer. We need to split the
        // connecting timeout to work for both channels before the global connecting timeout happens.
        // We choose about 45% of the global connecting timeout for this
        final Duration splittedDuration = clientConfig.getConnectingMinTimeout().dividedBy(100L).multipliedBy(45L);
        final Duration internalReconnectTimeout = clientConfig.getConnectingMinTimeout();
        connect(connection, splittedDuration, internalReconnectTimeout).thenAccept(
                status -> createConsumerChannelAndNotifySelf(status, consuming, self, splittedDuration));
    }

    @Override
    protected void doDisconnectClient(final Connection connection, @Nullable final ActorRef origin) {
        getSelf().tell((ClientDisconnected) () -> Optional.ofNullable(origin), origin);
    }

    @Override
    protected void allocateResourcesOnConnection(final ClientConnected clientConnected) {
        // nothing to do here
    }

    @Override
    protected CompletionStage<Status.Status> startConsumerActors(final ClientConnected clientConnected) {
        if (clientConnected instanceof RmqConsumerChannelCreated) {
            final RmqConsumerChannelCreated rmqConsumerChannelCreated = (RmqConsumerChannelCreated) clientConnected;
            startCommandConsumers(rmqConsumerChannelCreated.getChannel());
        }
        return super.startConsumerActors(clientConnected);
    }

    @Override
    protected void cleanupResourcesForConnection() {
        log.debug("cleaning up");
        stopCommandConsumers();
        stopChildActor(rmqPublisherActor);
        stopChildActor(rmqConnectionActor);
        rmqConnectionActor = null;
    }

    @Override
    protected ActorRef getPublisherActor() {
        return rmqPublisherActor;
    }

    private static Optional<ConnectionFactory> tryToCreateConnectionFactory(
            final RabbitConnectionFactoryFactory factoryFactory, final Connection connection,
            final RabbitMQExceptionHandler rabbitMQExceptionHandler) {

        try {
            return Optional.of(factoryFactory.createConnectionFactory(connection, rabbitMQExceptionHandler));
        } catch (final Throwable throwable) {
            // error creating factory; return early.)
            rabbitMQExceptionHandler.exceptionHandler.accept(throwable);
            return Optional.empty();
        }
    }

    private static Object messageFromConnectionStatus(final Status.Status status) {
        if (status instanceof Status.Failure) {
            final Status.Failure failure = (Status.Failure) status;
            return new ImmutableConnectionFailure(null, failure.cause(), null);
        } else {
            return (ClientConnected) Optional::empty;
        }
    }

    private CompletionStage<Status.Status> connect(final Connection connection, final Duration createChannelTimeout,
            final Duration internalReconnectTimeout) {

        final CompletableFuture<Status.Status> future = new CompletableFuture<>();
        if (rmqConnectionActor == null) {
            // complete the future if something went wrong during creation of the connection-factory-factory
            final RabbitMQExceptionHandler rabbitMQExceptionHandler = new RabbitMQExceptionHandler(
                    throwable -> future.complete(new Status.Failure(throwable)));

            final Optional<ConnectionFactory> connectionFactoryOpt = tryToCreateConnectionFactory(
                    rabbitConnectionFactoryFactory, connection, rabbitMQExceptionHandler);

            if (connectionFactoryOpt.isPresent()) {
                final ConnectionFactory connectionFactory = connectionFactoryOpt.get();

                final Props props = com.newmotion.akka.rabbitmq.ConnectionActor.props(connectionFactory,
                        FiniteDuration.apply(internalReconnectTimeout.getSeconds(), TimeUnit.SECONDS),
                        (rmqConnection, connectionActorRef) -> {
                            log.info("Established RMQ connection: {}", rmqConnection);
                            return null;
                        });

                rmqConnectionActor = startChildActorConflictFree(RMQ_CONNECTION_ACTOR_NAME, props);
                rmqPublisherActor = startRmqPublisherActor();

                // create publisher channel
                final CreateChannel createChannel = CreateChannel
                        .apply(ChannelActor.props((channel, channelActor) -> {
                            log.info(
                                    "Did set up publisher channel: {}. Telling the publisher actor the new channel",
                                    channel);
                            // provide the new channel to the publisher after the channel was connected (also includes reconnects)
                            if (rmqPublisherActor != null) {
                                final ChannelCreated channelCreated = new ChannelCreated(channelActor);
                                rmqPublisherActor.tell(channelCreated, channelActor);
                            }
                            return null;
                        }), Option.apply(PUBLISHER_CHANNEL));

                final Scheduler scheduler = getContext().system().scheduler();
                final ExecutionContext dispatcher = getContext().dispatcher();
                Patterns.ask(rmqConnectionActor, createChannel, createChannelTimeout).handle((reply, throwable) -> {
                    if (throwable != null) {
                        future.complete(new Status.Failure(throwable));
                    } else {
                        // waiting for "final RabbitMQExceptionHandler rabbitMQExceptionHandler" to get its chance to
                        // complete the future with an Exception before we report Status.Success right now
                        // so delay this by 1 second --
                        // with Java 9 this could be done more elegant with "orTimeout" or "completeOnTimeout" methods:
                        scheduler.scheduleOnce(Duration.ofSeconds(1L),
                                () -> future.complete(new Status.Success("channel created")), dispatcher);
                    }
                    return null;
                });
            }
        } else {
            log.debug("Connection <{}> is already open.", connectionId());
            future.complete(new Status.Success("already connected"));
        }
        return future;

    }

    private void createConsumerChannelAndNotifySelf(final Status.Status status, final boolean consuming,
            final ActorRef self, final Duration createChannelTimeout) {

        if (consuming && status instanceof Status.Success && null != rmqConnectionActor) {
            // send self the created channel
            final CreateChannel createChannel = CreateChannel.apply(ChannelActor.props(SendChannel.to(self)::apply),
                    Option.apply(CONSUMER_CHANNEL));
            // connection actor sends ChannelCreated; use an ASK to swallow the reply in which we are disinterested
            Patterns.ask(rmqConnectionActor, createChannel, createChannelTimeout);
        } else {
            final Object selfMessage = messageFromConnectionStatus(status);
            self.tell(selfMessage, self);
        }
    }

    private ActorRef startRmqPublisherActor() {
        stopChildActor(rmqPublisherActor);
        final Props publisherProps = RabbitMQPublisherActor.props(connectionId(), getTargetsOrEmptyList());
        return startChildActorConflictFree(RabbitMQPublisherActor.ACTOR_NAME, publisherProps);
    }

    @Override
    protected CompletionStage<Status.Status> startPublisherActor() {
        return CompletableFuture.completedFuture(DONE);
    }

    private void stopCommandConsumers() {
        consumedTagsToAddresses.clear();
        consumerByAddressWithIndex.forEach((addressWithIndex, child) -> stopChildActor(child));
        consumerByAddressWithIndex.clear();
    }

    private void startCommandConsumers(final Channel channel) {
        log.info("Starting to consume queues...");
        ensureQueuesExist(channel);
        stopCommandConsumers();
        startConsumers(channel);
    }

    private void startConsumers(final Channel channel) {
        getSourcesOrEmptyList().forEach(source -> source.getAddresses().forEach(sourceAddress -> {
            for (int i = 0; i < source.getConsumerCount(); i++) {
                final String addressWithIndex = sourceAddress + "-" + i;
                final AuthorizationContext authorizationContext = source.getAuthorizationContext();
                final Enforcement enforcement = source.getEnforcement().orElse(null);
                final HeaderMapping headerMapping = source.getHeaderMapping().orElse(null);
                final ActorRef consumer = startChildActorConflictFree(CONSUMER_ACTOR_PREFIX + addressWithIndex,
                        RabbitMQConsumerActor.props(sourceAddress, getMessageMappingProcessorActor(),
                                authorizationContext, enforcement, headerMapping, connectionId()));
                consumerByAddressWithIndex.put(addressWithIndex, consumer);
                try {
                    final String consumerTag = channel.basicConsume(sourceAddress, false,
                            new RabbitMQMessageConsumer(consumer, channel, sourceAddress));
                    log.debug("Consuming queue <{}>, consumer tag is <{}>.", addressWithIndex, consumerTag);
                    consumedTagsToAddresses.put(consumerTag, addressWithIndex);
                } catch (final IOException e) {
                    connectionLogger.failure("Failed to consume queue {0}: {1}", addressWithIndex, e.getMessage());
                    log.warning("Failed to consume queue <{}>: <{}>", addressWithIndex, e.getMessage());
                }
            }
        }));
    }

    private void ensureQueuesExist(final Channel channel) {
        final Collection<String> missingQueues = new ArrayList<>();
        getSourcesOrEmptyList().forEach(consumer -> consumer.getAddresses().forEach(address -> {
            try {
                channel.queueDeclarePassive(address);
            } catch (final IOException e) {
                missingQueues.add(address);
                log.warning("The queue <{}> does not exist.", address);
            } catch (final AlreadyClosedException e) {
                if (!missingQueues.isEmpty()) {
                    // Our client will automatically close the connection if a queue does not exists. This will
                    // cause an AlreadyClosedException for the following queue (e.g. ['existing1', 'notExisting', -->'existing2'])
                    // That's why we will ignore this error if the missingQueues list isn't empty.
                    log.warning(
                            "Received exception of type {} when trying to declare queue {}. This happens when a previous "
                                    + "queue was missing and thus the connection got closed.",
                            e.getClass().getName(), address);
                } else {
                    log.error("Exception while declaring queue {}", address, e);
                    throw e;
                }
            }
        }));
        if (!missingQueues.isEmpty()) {
            log.warning("Stopping RMQ client actor for connection <{}> as queues to connect to are missing: <{}>",
                    connectionId(), missingQueues);
            connectionLogger.failure("Can not connect to RabbitMQ as queues are missing: {0}", missingQueues);
            throw ConnectionFailedException.newBuilder(connectionId())
                    .description("The queues " + missingQueues + " to connect to are missing.").build();
        }
    }

    /**
     * Custom exception handler which handles exception during connection.
     */
    private static final class RabbitMQExceptionHandler extends DefaultExceptionHandler {

        private final Consumer<Throwable> exceptionHandler;

        private RabbitMQExceptionHandler(final Consumer<Throwable> exceptionHandler) {
            this.exceptionHandler = exceptionHandler;
        }

        @Override
        public void handleUnexpectedConnectionDriverException(final com.rabbitmq.client.Connection conn,
                final Throwable exception) {

            // establishing the connection was not possible (maybe wrong host, port, credentials, ...)
            exceptionHandler.accept(exception);
        }

    }

    private static final class RmqConsumerChannelCreated implements ClientConnected {

        private final Channel channel;

        private RmqConsumerChannelCreated(final Channel channel) {
            this.channel = channel;
        }

        private Channel getChannel() {
            return channel;
        }

        @Override
        public Optional<ActorRef> getOrigin() {
            return Optional.empty();
        }

    }

    private static final class SendChannel implements FI.Apply2<Channel, ActorRef, Object> {

        private final ActorRef recipient;

        private SendChannel(final ActorRef recipient) {
            this.recipient = recipient;
        }

        private static SendChannel to(final ActorRef recipient) {
            return new SendChannel(recipient);
        }

        @Override
        public Object apply(final Channel channel, final ActorRef channelActor) {
            recipient.tell(new RmqConsumerChannelCreated(channel), channelActor);
            return channel;
        }

    }

    /**
     * Custom consumer which is notified about different events related to the connection in order to track connectivity
     * status.
     */
    private final class RabbitMQMessageConsumer extends DefaultConsumer {

        private final ActorRef consumerActor;
        private final String address;

        /**
         * Constructs a new instance and records its association to the passed-in channel.
         *
         * @param consumerActor the ActorRef to the consumer actor
         * @param channel the channel to which this consumer is attached
         * @param address the address of the consumer
         */
        private RabbitMQMessageConsumer(final ActorRef consumerActor, final Channel channel, final String address) {
            super(channel);
            this.consumerActor = consumerActor;
            this.address = address;
            updateSourceStatus(ConnectivityStatus.OPEN, "Consumer initialized at " + Instant.now());
        }

        @Override
        public void handleDelivery(final String consumerTag, final Envelope envelope,
                final AMQP.BasicProperties properties, final byte[] body) {

            ConnectionLogUtil.enhanceLogWithConnectionId(log, connectionId());
            try {
                consumerActor.tell(new Delivery(envelope, properties, body), RabbitMQClientActor.this.getSelf());
            } catch (final Exception e) {
                connectionLogger.failure("Failed to process delivery {0}: {1}", envelope.getDeliveryTag(),
                        e.getMessage());
                log.info("Failed to process delivery <{}>: {}", envelope.getDeliveryTag(), e.getMessage());
            } finally {
                try {
                    getChannel().basicAck(envelope.getDeliveryTag(), false);
                } catch (final IOException e) {
                    connectionLogger.failure("Failed to ack delivery {0}: {1}", envelope.getDeliveryTag(),
                            e.getMessage());
                    log.info("Failed to ack delivery <{}>: {}", envelope.getDeliveryTag(), e.getMessage());
                }
            }
        }

        @Override
        public void handleConsumeOk(final String consumerTag) {
            super.handleConsumeOk(consumerTag);
            ConnectionLogUtil.enhanceLogWithConnectionId(log, connectionId());

            final String consumingQueueByTag = consumedTagsToAddresses.get(consumerTag);
            if (null != consumingQueueByTag) {
                connectionLogger.success("Consume OK for consumer queue {0}", consumingQueueByTag);
                log.info("Consume OK for consumer queue <{}> on connection <{}>.", consumingQueueByTag,
                        connectionId());
            }

            updateSourceStatus(ConnectivityStatus.OPEN, "Consumer started at " + Instant.now());
        }

        @Override
        public void handleCancel(final String consumerTag) throws IOException {
            super.handleCancel(consumerTag);
            ConnectionLogUtil.enhanceLogWithConnectionId(log, connectionId());

            final String consumingQueueByTag = consumedTagsToAddresses.get(consumerTag);
            if (null != consumingQueueByTag) {
                connectionLogger.failure("Consumer with queue {0} was cancelled. This can happen for example "
                        + "when the queue was deleted.", consumingQueueByTag);
                log.warning("Consumer with queue <{}> was cancelled on connection <{}>. This can happen for "
                        + "example when the queue was deleted.", consumingQueueByTag, connectionId());
            }

            updateSourceStatus(ConnectivityStatus.FAILED, "Consumer for queue cancelled at " + Instant.now());
        }

        @Override
        public void handleShutdownSignal(final String consumerTag, final ShutdownSignalException sig) {
            super.handleShutdownSignal(consumerTag, sig);
            ConnectionLogUtil.enhanceLogWithConnectionId(log, connectionId());

            final String consumingQueueByTag = consumedTagsToAddresses.get(consumerTag);
            if (null != consumingQueueByTag) {
                connectionLogger.failure(
                        "Consumer with queue <{}> shutdown as the channel or the underlying connection has "
                                + "been shut down.",
                        consumingQueueByTag);
                log.warning("Consumer with queue <{}> shutdown as the channel or the underlying connection has "
                        + "been shut down on connection <{}>.", consumingQueueByTag, connectionId());
            }

            updateSourceStatus(ConnectivityStatus.FAILED,
                    "Channel or the underlying connection has been shut down at " + Instant.now());
        }

        @Override
        public void handleRecoverOk(final String consumerTag) {
            super.handleRecoverOk(consumerTag);
            ConnectionLogUtil.enhanceLogWithConnectionId(log, connectionId());

            log.info("Recovered OK for consumer with tag <{}> on connection <{}>", consumerTag, connectionId());

            getSelf().tell((ClientConnected) Optional::empty, getSelf());
        }

        private void updateSourceStatus(final ConnectivityStatus connectionStatus, final String statusDetails) {
            consumerActor
                    .tell(ConnectivityModelFactory.newStatusUpdate(InstanceIdentifierSupplier.getInstance().get(),
                            connectionStatus, address, statusDetails, Instant.now()), ActorRef.noSender());
        }

    }

}