Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.connectivity.service.messaging.amqp;
import java.net.URI;
import java.text.MessageFormat;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import org.apache.qpid.jms.JmsConnection;
import org.apache.qpid.jms.JmsConnectionListener;
import org.apache.qpid.jms.JmsSession;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.apache.qpid.jms.provider.ProviderFactory;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.connectivity.api.BaseClientState;
import org.eclipse.ditto.connectivity.model.Connection;
import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException;
import org.eclipse.ditto.connectivity.model.ConnectionType;
import org.eclipse.ditto.connectivity.model.ConnectivityStatus;
import org.eclipse.ditto.connectivity.model.MetricDirection;
import org.eclipse.ditto.connectivity.model.MetricType;
import org.eclipse.ditto.connectivity.model.RecoveryStatus;
import org.eclipse.ditto.connectivity.model.signals.commands.exceptions.ConnectionFailedException;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.TestConnection;
import org.eclipse.ditto.connectivity.service.config.Amqp10Config;
import org.eclipse.ditto.connectivity.service.config.ClientConfig;
import org.eclipse.ditto.connectivity.service.config.ConnectionConfig;
import org.eclipse.ditto.connectivity.service.messaging.BaseClientActor;
import org.eclipse.ditto.connectivity.service.messaging.BaseClientData;
import org.eclipse.ditto.connectivity.service.messaging.amqp.status.ConnectionFailureStatusReport;
import org.eclipse.ditto.connectivity.service.messaging.amqp.status.ConnectionRestoredStatusReport;
import org.eclipse.ditto.connectivity.service.messaging.amqp.status.ConsumerClosedStatusReport;
import org.eclipse.ditto.connectivity.service.messaging.amqp.status.ProducerClosedStatusReport;
import org.eclipse.ditto.connectivity.service.messaging.amqp.status.SessionClosedStatusReport;
import org.eclipse.ditto.connectivity.service.messaging.internal.AbstractWithOrigin;
import org.eclipse.ditto.connectivity.service.messaging.internal.ClientConnected;
import org.eclipse.ditto.connectivity.service.messaging.internal.CloseSession;
import org.eclipse.ditto.connectivity.service.messaging.internal.ConnectClient;
import org.eclipse.ditto.connectivity.service.messaging.internal.ConnectionFailure;
import org.eclipse.ditto.connectivity.service.messaging.internal.DisconnectClient;
import org.eclipse.ditto.connectivity.service.messaging.internal.RecoverSession;
import org.eclipse.ditto.connectivity.service.messaging.internal.RetrieveAddressStatus;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.ConnectionLogger;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.metrics.ThrottledLoggerMetricsAlert;
import org.eclipse.ditto.connectivity.service.messaging.tunnel.SshTunnelState;
import org.eclipse.ditto.connectivity.service.util.ConnectivityMdcEntryKey;
import org.eclipse.ditto.internal.utils.akka.logging.ThreadSafeDittoLoggingAdapter;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import akka.Done;
import akka.NotUsed;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.FSM;
import akka.actor.Props;
import akka.actor.Status;
import akka.japi.pf.FSMStateFunctionBuilder;
import akka.pattern.Patterns;
import akka.stream.javadsl.Sink;
/**
* Actor which manages a connection to an AMQP 1.0 server using the Qpid JMS client.
* This actor delegates interaction with the JMS client to a child actor because the JMS client blocks in most cases
* which does not work well with actors.
*/
public final class AmqpClientActor extends BaseClientActor implements ExceptionListener {
private static final String SPEC_CONFIG_RECOVER_ON_SESSION_CLOSED = "recover.on-session-closed";
private static final String SPEC_CONFIG_RECOVER_ON_CONNECTION_RESTORED = "recover.on-connection-restored";
private final JmsConnectionFactory jmsConnectionFactory;
final StatusReportingListener connectionListener;
@Nullable private JmsConnection jmsConnection;
@Nullable private Session jmsSession;
@Nullable private ActorRef testConnectionHandler;
@Nullable private ActorRef connectConnectionHandler;
@Nullable private ActorRef disconnectConnectionHandler;
private final Map consumerByNamePrefix;
private final boolean recoverSessionOnSessionClosed;
private final boolean recoverSessionOnConnectionRestored;
private final Duration clientAskTimeout;
private final Duration initialConsumerResourceStatusAskTimeout;
private ActorRef amqpPublisherActor;
/*
* This constructor is called via reflection by the static method props.
*/
@SuppressWarnings("unused")
private AmqpClientActor(final Connection connection,
final ActorRef commandForwarderActor,
final ActorRef connectionActor,
final Config connectivityConfigOverwrites,
final boolean dryRun) {
super(connection, commandForwarderActor, connectionActor, dryRun, connectivityConfigOverwrites);
final ConnectionConfig connectionConfig = connectivityConfig().getConnectionConfig();
final Amqp10Config amqp10Config = connectionConfig.getAmqp10Config();
jmsConnectionFactory =
ConnectionBasedJmsConnectionFactory.getInstance(AmqpSpecificConfig.toDefaultConfig(amqp10Config),
this::getSshTunnelState, getContext().getSystem());
connectionListener = new StatusReportingListener(getSelf(), logger, connectionLogger);
consumerByNamePrefix = new HashMap<>();
recoverSessionOnSessionClosed = isRecoverSessionOnSessionClosedEnabled(connection);
recoverSessionOnConnectionRestored = isRecoverSessionOnConnectionRestoredEnabled(connection);
clientAskTimeout = connectionConfig.getClientActorAskTimeout();
initialConsumerResourceStatusAskTimeout = amqp10Config.getInitialConsumerStatusAskTimeout();
connectionCounterRegistry.registerAlertFactory(ConnectionType.AMQP_10, MetricType.THROTTLED,
MetricDirection.INBOUND,
ThrottledLoggerMetricsAlert.getFactory(
address -> connectionLoggerRegistry.forInboundThrottled(connection, address)));
}
/*
* This constructor is called via reflection by the static method props.
*/
@SuppressWarnings("unused")
private AmqpClientActor(final Connection connection,
final JmsConnectionFactory jmsConnectionFactory,
final ActorRef commandForwarderActor,
final ActorRef connectionActor,
final boolean dryRun) {
super(connection, commandForwarderActor, connectionActor, dryRun, ConfigFactory.empty());
this.jmsConnectionFactory = jmsConnectionFactory;
connectionListener = new StatusReportingListener(getSelf(), logger, connectionLogger);
consumerByNamePrefix = new HashMap<>();
recoverSessionOnSessionClosed = isRecoverSessionOnSessionClosedEnabled(connection);
recoverSessionOnConnectionRestored = isRecoverSessionOnConnectionRestoredEnabled(connection);
clientAskTimeout = Duration.ofSeconds(10L);
initialConsumerResourceStatusAskTimeout = Duration.ofMillis(500L);
}
/**
* Creates Akka configuration object for this actor.
*
* @param connection the connection.
* @param commandForwarderActor the actor used to send signals into the ditto cluster.
* @param connectionActor the connectionPersistenceActor which created this client.
* @param configOverwrites an override for the default connectivity config values -
* @param actorSystem the actor system.
* as Typesafe {@code Config} because this one is serializable in Akka by default.
* @param dittoHeaders headers of the command that caused this actor to be created.
* @return the Akka configuration Props object.
*/
public static Props props(final Connection connection, final ActorRef commandForwarderActor,
final ActorRef connectionActor, final Config configOverwrites, final ActorSystem actorSystem,
final DittoHeaders dittoHeaders) {
return Props.create(AmqpClientActor.class, validateConnection(connection, actorSystem),
commandForwarderActor, connectionActor, configOverwrites, dittoHeaders.isDryRun());
}
/**
* Creates Akka configuration object for this actor.
*
* @param connection connection parameters.
* @param commandForwarderActor the actor used to send signals into the ditto cluster.
* @param connectionActor the connectionPersistenceActor which created this client.
* @param jmsConnectionFactory the JMS connection factory.
* @param actorSystem the actor system.
* @return the Akka configuration Props object.
*/
static Props propsForTest(final Connection connection, @Nullable final ActorRef commandForwarderActor,
final ActorRef connectionActor, final JmsConnectionFactory jmsConnectionFactory,
final ActorSystem actorSystem) {
return Props.create(AmqpClientActor.class, validateConnection(connection, actorSystem),
jmsConnectionFactory, commandForwarderActor, connectionActor, false);
}
private static Connection validateConnection(final Connection connection, final ActorSystem actorSystem) {
try {
final String connectionUri = ConnectionBasedJmsConnectionFactory.buildAmqpConnectionUri(connection,
connection.getId().toString(),
// fake established tunnel state for uri validation
() -> SshTunnelState.from(connection).established(22222),
Map.of(),
SaslPlainCredentialsSupplier.of(actorSystem));
ProviderFactory.create(URI.create(connectionUri));
// it is safe to pass an empty map as default config as only default values are loaded via that config
// of which we can be certain that they are always valid
return connection;
} catch (final Exception e) {
final String msgPattern = "Failed to instantiate an amqp provider from the given configuration: {0}";
throw ConnectionConfigurationInvalidException
.newBuilder(MessageFormat.format(msgPattern, e.getMessage()))
.description(e.getMessage())
.cause(e)
.build();
}
}
@Override
public void postStop() {
super.postStop();
ensureJmsConnectionClosed();
}
@Override
protected Set getExcludedAddressReportingChildNamePatterns() {
final Set excludedChildNamePatterns =
new HashSet<>(super.getExcludedAddressReportingChildNamePatterns());
excludedChildNamePatterns.add(
Pattern.compile(Pattern.quote(JMSConnectionHandlingActor.ACTOR_NAME_PREFIX) + ".*"));
return excludedChildNamePatterns;
}
@Override
protected FSMStateFunctionBuilder inConnectedState() {
return super.inConnectedState()
.event(JmsSessionRecovered.class, this::handleSessionRecovered);
}
@Override
protected FSMStateFunctionBuilder inAnyState() {
return super.inAnyState()
.event(ConnectionRestoredStatusReport.class,
(report, currentData) -> handleConnectionRestored(currentData))
.event(ConnectionFailureStatusReport.class, this::handleConnectionFailure)
.event(ConsumerClosedStatusReport.class, this::handleConsumerClosed)
.event(ProducerClosedStatusReport.class, this::handleProducerClosed)
.event(SessionClosedStatusReport.class, this::handleSessionClosed);
}
@Override
protected CompletionStage doTestConnection(final TestConnection testConnectionCommand) {
// delegate to child actor because the QPID JMS client is blocking until connection is opened/closed
final Connection connectionToBeTested = testConnectionCommand.getConnection();
final ClientConfig clientConfig = connectivityConfig().getClientConfig();
return Patterns.ask(getTestConnectionHandler(connectionToBeTested),
jmsConnect(getSender(), connectionToBeTested), clientConfig.getTestingTimeout())
// compose the disconnect because otherwise the actor hierarchy might be stopped too fast
.thenCompose(response -> {
logger.withCorrelationId(testConnectionCommand)
.withMdcEntry(ConnectivityMdcEntryKey.CONNECTION_ID, connectionToBeTested.getId())
.debug("Closing AMQP 1.0 connection after testing connection.");
if (response instanceof JmsConnected jmsConnected) {
final JmsConnection connectedJmsConnection = jmsConnected.connection;
final JmsDisconnect jmsDisconnect = new JmsDisconnect(ActorRef.noSender(),
connectedJmsConnection, true);
return Patterns.ask(getDisconnectConnectionHandler(connectionToBeTested), jmsDisconnect,
clientConfig.getTestingTimeout())
// replace jmsDisconnected message with original response
.thenApply(jmsDisconnected -> response);
} else {
return CompletableFuture.completedFuture(response);
}
})
.handle((response, throwable) -> {
if (throwable != null || response instanceof Status.Failure || response instanceof Throwable) {
final Throwable ex =
response instanceof Status.Failure ? ((Status.Failure) response).cause() :
response instanceof Throwable ? (Throwable) response : throwable;
final ConnectionFailedException failedException =
ConnectionFailedException.newBuilder(connectionId())
.description("The requested Connection could not be connected due to '" +
ex.getClass().getSimpleName() + ": " + ex.getMessage() + "'")
.cause(ex).build();
return new Status.Failure(failedException);
} else if (response instanceof ConnectionFailure connectionFailure) {
return connectionFailure.getFailure();
} else {
return new Status.Success(response);
}
});
}
@Override
protected CompletionStage stopConsuming() {
final var timeout = Duration.ofMinutes(2L);
final CompletableFuture[] futures = streamConsumerActors()
.map(consumer -> Patterns.ask(consumer, AmqpConsumerActor.Control.STOP_CONSUMER, timeout))
.map(CompletionStage::toCompletableFuture)
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(futures);
}
@Override
protected void doConnectClient(final Connection connection, @Nullable final ActorRef origin) {
// delegate to child actor because the QPID JMS client is blocking until connection is opened/closed
getConnectConnectionHandler(connection).tell(jmsConnect(origin, connection), getSelf());
}
@Override
protected void doDisconnectClient(final Connection connection, @Nullable final ActorRef origin,
final boolean shutdownAfterDisconnect) {
// delegate to child actor because the QPID JMS client is blocking until connection is opened/closed
getDisconnectConnectionHandler(connection)
.tell(new JmsDisconnect(origin, jmsConnection, shutdownAfterDisconnect), getSelf());
}
@Override
protected void allocateResourcesOnConnection(final ClientConnected clientConnected) {
if (clientConnected instanceof JmsConnected client) {
logger.info("Received JmsConnected");
ensureJmsConnectionClosed();
jmsConnection = client.connection;
jmsConnection.addConnectionListener(connectionListener);
jmsSession = client.session;
} else {
logger.info(
"ClientConnected was not JmsConnected as expected, ignoring as this probably was a reconnection");
}
}
@Override
protected CompletionStage startPublisherActor() {
final CompletableFuture future = new CompletableFuture<>();
stopChildActor(amqpPublisherActor);
if (null != jmsSession) {
final Props props = AmqpPublisherActor.props(connection(),
jmsSession,
connectivityStatusResolver,
connectivityConfig());
amqpPublisherActor = startChildActorConflictFree(AmqpPublisherActor.ACTOR_NAME_PREFIX, props);
Patterns.ask(amqpPublisherActor, AmqpPublisherActor.Control.INITIALIZE, clientAskTimeout)
.whenComplete((result, error) -> {
if (error != null) {
future.completeExceptionally(error);
} else if (result instanceof Throwable throwable) {
future.completeExceptionally(throwable);
} else {
future.complete(DONE);
}
});
} else {
future.completeExceptionally(ConnectionFailedException
.newBuilder(connectionId())
.message("Could not start publisher actor due to missing AMQP 1.0 session or connection!")
.build());
}
return future;
}
@Override
protected CompletionStage startConsumerActors(@Nullable final ClientConnected clientConnected) {
if (clientConnected instanceof JmsConnected jmsConnected) {
final ActorRef jmsActor = getConnectConnectionHandler(connection());
return startCommandConsumers(jmsConnected.consumerList, jmsActor)
.thenApply(ignored -> new Status.Success(Done.getInstance()));
}
return CompletableFuture.completedFuture(new Status.Success(Done.getInstance()));
}
@Override
protected void cleanupResourcesForConnection() {
logger.debug("Cleaning up resources for connection <{}>.", connectionId());
stopCommandConsumers();
stopChildActor(amqpPublisherActor);
// closing JMS connection closes all sessions and consumers
ensureJmsConnectionClosed();
jmsConnection = null;
jmsSession = null;
}
/*
* Kill connection handlers on timeout to be able to handle the next command immediately.
*/
@Override
protected void cleanupFurtherResourcesOnConnectionTimeout(final BaseClientState currentState) {
if (connectConnectionHandler != null) {
stopChildActor(connectConnectionHandler);
connectConnectionHandler = null;
}
if (disconnectConnectionHandler != null) {
stopChildActor(disconnectConnectionHandler);
disconnectConnectionHandler = null;
}
super.cleanupFurtherResourcesOnConnectionTimeout(currentState);
}
@Override
public void onException(final JMSException exception) {
connectionLogger.exception("Exception occurred: {0}", exception.getMessage());
logger.warning("{} occurred: {}", exception.getClass().getName(), exception.getMessage());
}
@Override
protected ActorRef getPublisherActor() {
return amqpPublisherActor;
}
private CompletionStage startCommandConsumers(final List consumers, final ActorRef jmsActor) {
if (isConsuming()) {
stopCommandConsumers();
// wait a fraction of the configured timeout before asking to allow the consumer to stabilize
final CompletionStage