org.eclipse.ditto.connectivity.service.messaging.persistence.ConnectionPersistenceActor Maven / Gradle / Ivy
/*
* 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.persistence;
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;
import static org.eclipse.ditto.connectivity.api.ConnectivityMessagingConstants.CLUSTER_ROLE;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeExceptionBuilder;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.headers.DittoHeadersBuilder;
import org.eclipse.ditto.base.model.headers.WithDittoHeaders;
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
import org.eclipse.ditto.base.model.signals.Signal;
import org.eclipse.ditto.base.model.signals.SignalWithEntityId;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.connectivity.api.BaseClientState;
import org.eclipse.ditto.connectivity.api.commands.sudo.ConnectivitySudoCommand;
import org.eclipse.ditto.connectivity.api.commands.sudo.SudoRetrieveClientActorProps;
import org.eclipse.ditto.connectivity.model.Connection;
import org.eclipse.ditto.connectivity.model.ConnectionId;
import org.eclipse.ditto.connectivity.model.ConnectionLifecycle;
import org.eclipse.ditto.connectivity.model.ConnectionMetrics;
import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory;
import org.eclipse.ditto.connectivity.model.ConnectivityStatus;
import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand;
import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommandInterceptor;
import org.eclipse.ditto.connectivity.model.signals.commands.exceptions.ConnectionFailedException;
import org.eclipse.ditto.connectivity.model.signals.commands.exceptions.ConnectionNotAccessibleException;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.CheckConnectionLogsActive;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.CloseConnection;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.EnableConnectionLogs;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.OpenConnection;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.TestConnection;
import org.eclipse.ditto.connectivity.model.signals.commands.modify.TestConnectionResponse;
import org.eclipse.ditto.connectivity.model.signals.commands.query.ConnectivityQueryCommand;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionLogs;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionLogsResponse;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionMetrics;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionMetricsResponse;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionStatus;
import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionStatusResponse;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectionClosed;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectionCreated;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectionDeleted;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectionModified;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectionOpened;
import org.eclipse.ditto.connectivity.model.signals.events.ConnectivityEvent;
import org.eclipse.ditto.connectivity.service.config.ConnectionConfig;
import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig;
import org.eclipse.ditto.connectivity.service.config.MonitoringConfig;
import org.eclipse.ditto.connectivity.service.config.MqttConfig;
import org.eclipse.ditto.connectivity.service.messaging.ClientActorId;
import org.eclipse.ditto.connectivity.service.messaging.ClientActorPropsArgs;
import org.eclipse.ditto.connectivity.service.messaging.ClientActorPropsFactory;
import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpValidator;
import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushValidator;
import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaValidator;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.ConnectionLogger;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.ConnectionLoggerRegistry;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.InfoProviderFactory;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.RetrieveConnectionLogsAggregatorActor;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.metrics.RetrieveConnectionMetricsAggregatorActor;
import org.eclipse.ditto.connectivity.service.messaging.monitoring.metrics.RetrieveConnectionStatusAggregatorActor;
import org.eclipse.ditto.connectivity.service.messaging.mqtt.Mqtt3Validator;
import org.eclipse.ditto.connectivity.service.messaging.mqtt.Mqtt5Validator;
import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.ConnectionAction;
import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.ConnectionState;
import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.StagedCommand;
import org.eclipse.ditto.connectivity.service.messaging.persistence.strategies.commands.ConnectionCreatedStrategies;
import org.eclipse.ditto.connectivity.service.messaging.persistence.strategies.commands.ConnectionDeletedStrategies;
import org.eclipse.ditto.connectivity.service.messaging.persistence.strategies.events.ConnectionEventStrategies;
import org.eclipse.ditto.connectivity.service.messaging.rabbitmq.RabbitMQValidator;
import org.eclipse.ditto.connectivity.service.messaging.validation.CompoundConnectivityCommandInterceptor;
import org.eclipse.ditto.connectivity.service.messaging.validation.ConnectionValidator;
import org.eclipse.ditto.connectivity.service.messaging.validation.CustomConnectivityCommandInterceptorProvider;
import org.eclipse.ditto.connectivity.service.messaging.validation.DittoConnectivityCommandValidator;
import org.eclipse.ditto.connectivity.service.util.ConnectionPubSub;
import org.eclipse.ditto.connectivity.service.util.ConnectivityMdcEntryKey;
import org.eclipse.ditto.internal.utils.akka.PingCommand;
import org.eclipse.ditto.internal.utils.akka.logging.CommonMdcEntryKey;
import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter;
import org.eclipse.ditto.internal.utils.akka.logging.DittoLoggerFactory;
import org.eclipse.ditto.internal.utils.cluster.DistPubSubAccess;
import org.eclipse.ditto.internal.utils.cluster.ShardedBinaryEnvelope;
import org.eclipse.ditto.internal.utils.cluster.StopShardedActor;
import org.eclipse.ditto.internal.utils.config.InstanceIdentifierSupplier;
import org.eclipse.ditto.internal.utils.config.ScopedConfig;
import org.eclipse.ditto.internal.utils.metrics.DittoMetrics;
import org.eclipse.ditto.internal.utils.persistence.mongo.config.ActivityCheckConfig;
import org.eclipse.ditto.internal.utils.persistence.mongo.config.SnapshotConfig;
import org.eclipse.ditto.internal.utils.persistence.mongo.streaming.MongoReadJournal;
import org.eclipse.ditto.internal.utils.persistentactors.AbstractPersistenceActor;
import org.eclipse.ditto.internal.utils.persistentactors.EmptyEvent;
import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy;
import org.eclipse.ditto.internal.utils.persistentactors.commands.DefaultContext;
import org.eclipse.ditto.internal.utils.persistentactors.events.EventStrategy;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.thingsearch.model.signals.commands.subscription.CreateSubscription;
import com.typesafe.config.Config;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.PoisonPill;
import akka.actor.Props;
import akka.actor.Status;
import akka.actor.SupervisorStrategy;
import akka.cluster.Cluster;
import akka.japi.pf.ReceiveBuilder;
import akka.pattern.Patterns;
import akka.persistence.RecoveryCompleted;
/**
* Handles {@code *Connection} commands and manages the persistence of connection. The actual connection handling to the
* remote server is delegated to a child actor that uses a specific client (AMQP 1.0 or 0.9.1).
*/
public final class ConnectionPersistenceActor
extends AbstractPersistenceActor, Connection, ConnectionId, ConnectionState,
ConnectivityEvent> {
/**
* Prefix to prepend to the connection ID to construct the persistence ID.
*/
public static final String PERSISTENCE_ID_PREFIX = "connection:";
/**
* The ID of the journal plugin this persistence actor uses.
*/
public static final String JOURNAL_PLUGIN_ID = "akka-contrib-mongodb-persistence-connection-journal";
/**
* The ID of the snapshot plugin this persistence actor uses.
*/
public static final String SNAPSHOT_PLUGIN_ID = "akka-contrib-mongodb-persistence-connection-snapshots";
private static final Duration DEFAULT_RETRIEVE_STATUS_TIMEOUT = Duration.ofMillis(500L);
private static final Duration GRACE_PERIOD_NOT_RETRIEVING_CONNECTION_STATUS_AFTER_RECOVERY = Duration.ofSeconds(10);
private static final Duration SELF_RETRIEVE_CONNECTION_STATUS_TIMEOUT = Duration.ofSeconds(30);
// never retry, just escalate. ConnectionSupervisorActor will handle restarting this actor
private static final SupervisorStrategy ESCALATE_ALWAYS_STRATEGY = OneForOneEscalateStrategy.escalateStrategy();
private final Cluster cluster;
private final ActorRef commandForwarderActor;
private final ClientActorPropsFactory propsFactory;
private final ActorRef pubSubMediator;
private final ConnectionLoggerRegistry connectionLoggerRegistry;
private final ConnectionLogger connectionLogger;
private final Duration clientActorAskTimeout;
private final Duration checkLoggingActiveInterval;
private final Duration loggingEnabledDuration;
private final ConnectivityConfig connectivityConfig;
private final Config connectivityConfigOverwrites;
private final ConnectionPriorityProvider connectionPriorityProvider;
private final ConnectivityCommandInterceptor commandValidator;
private final ConnectionPubSub connectionPubSub;
private final ActorRef clientShardRegion;
private int subscriptionCounter = 0;
private int ongoingStagedCommands = 0;
private boolean inCoordinatedShutdown = false;
private Instant connectionClosedAt = Instant.now();
private boolean connectionOpened;
@Nullable private Instant loggingEnabledUntil;
@Nullable private Integer priority;
@Nullable private Instant recoveredAt;
ConnectionPersistenceActor(final ConnectionId connectionId,
final ActorRef commandForwarderActor,
final ActorRef pubSubMediator,
final Config connectivityConfigOverwrites,
final ActorRef clientShardRegion) {
super(connectionId);
final ActorSystem actorSystem = context().system();
cluster = Cluster.get(actorSystem);
final Config dittoExtensionConfig = ScopedConfig.dittoExtension(actorSystem.settings().config());
this.commandForwarderActor = commandForwarderActor;
propsFactory = ClientActorPropsFactory.get(actorSystem, dittoExtensionConfig);
this.pubSubMediator = pubSubMediator;
this.connectivityConfigOverwrites = connectivityConfigOverwrites;
this.clientShardRegion = clientShardRegion;
connectivityConfig = getConnectivityConfigWithOverwrites(connectivityConfigOverwrites);
commandValidator = getCommandValidator();
final ConnectionConfig connectionConfig = connectivityConfig.getConnectionConfig();
connectionPriorityProvider = ConnectionPriorityProviderFactory.get(actorSystem, dittoExtensionConfig)
.newProvider(self(), log);
clientActorAskTimeout = connectionConfig.getClientActorAskTimeout();
final MonitoringConfig monitoringConfig = connectivityConfig.getMonitoringConfig();
connectionLoggerRegistry = ConnectionLoggerRegistry.fromConfig(monitoringConfig.logger());
connectionLogger = connectionLoggerRegistry.forConnection(connectionId);
loggingEnabledDuration = monitoringConfig.logger().logDuration();
checkLoggingActiveInterval = monitoringConfig.logger().loggingActiveCheckInterval();
connectionPubSub = ConnectionPubSub.get(actorSystem);
// Make duration fuzzy to avoid all connections getting updated at once.
final Duration fuzzyPriorityUpdateInterval =
makeFuzzy(connectivityConfig.getConnectionConfig().getPriorityUpdateInterval());
startUpdatePriorityPeriodically(fuzzyPriorityUpdateInterval);
}
private ConnectivityConfig getConnectivityConfigWithOverwrites(final Config connectivityConfigOverwrites) {
final Config defaultConfig = getContext().getSystem().settings().config();
final Config withOverwrites = connectivityConfigOverwrites.withFallback(defaultConfig);
return ConnectivityConfig.of(withOverwrites);
}
/**
* Makes the duration fuzzy. By returning a duration which is something between 5% shorter or 5% longer.
*
* @param duration the duration to make fuzzy.
* @return the fuzzy duration.
*/
private static Duration makeFuzzy(final Duration duration) {
final long millis = duration.toMillis();
final double fuzzyFactor =
new Random().doubles(0.95, 1.05).findAny().orElse(0.0);
final double fuzzedMillis = millis * fuzzyFactor;
return Duration.ofMillis((long) fuzzedMillis);
}
@Override
protected DittoDiagnosticLoggingAdapter createLogger() {
return DittoLoggerFactory.getDiagnosticLoggingAdapter(this)
.withMdcEntry(ConnectivityMdcEntryKey.CONNECTION_ID.toString(), entityId);
}
/**
* Creates Akka configuration object for this actor.
*
* @param connectionId the connection ID.
* @param commandForwarderActor the actor used to send signals into the ditto cluster..
* @param connectivityConfigOverwrites the overwrites for the connectivity config for the given connection.
* @return the Akka configuration Props object.
*/
public static Props props(final ConnectionId connectionId,
final ActorRef commandForwarderActor,
final ActorRef pubSubMediator,
final Config connectivityConfigOverwrites,
final ActorRef clientShardRegion) {
return Props.create(ConnectionPersistenceActor.class, connectionId, commandForwarderActor, pubSubMediator,
connectivityConfigOverwrites, clientShardRegion);
}
/**
* Compute the length of the subscription ID prefix to identify the client actor owning a search session.
*
* @param clientCount the client count of the connection.
* @return the length of the subscription prefix.
*/
public static int getSubscriptionPrefixLength(final int clientCount) {
return Integer.toHexString(clientCount).length();
}
@Override
public String persistenceId() {
return PERSISTENCE_ID_PREFIX + entityId;
}
@Override
public String journalPluginId() {
return JOURNAL_PLUGIN_ID;
}
@Override
public String snapshotPluginId() {
return SNAPSHOT_PLUGIN_ID;
}
@Override
protected Class getEventClass() {
return ConnectivityEvent.class;
}
@Override
protected CommandStrategy.Context getStrategyContext() {
return DefaultContext.getInstance(
ConnectionState.of(entityId, connectionLoggerRegistry, connectionLogger, commandValidator), log);
}
@Override
protected ConnectionCreatedStrategies getCreatedStrategy() {
return ConnectionCreatedStrategies.getInstance();
}
@Override
protected ConnectionDeletedStrategies getDeletedStrategy() {
return ConnectionDeletedStrategies.getInstance();
}
@Override
protected EventStrategy, Connection> getEventStrategy() {
return ConnectionEventStrategies.getInstance();
}
@Override
protected ActivityCheckConfig getActivityCheckConfig() {
return connectivityConfig.getConnectionConfig().getActivityCheckConfig();
}
@Override
protected SnapshotConfig getSnapshotConfig() {
return connectivityConfig.getConnectionConfig().getSnapshotConfig();
}
@Override
protected boolean entityExistsAsDeleted() {
return entity != null &&
entity.getLifecycle().orElse(ConnectionLifecycle.ACTIVE) == ConnectionLifecycle.DELETED;
}
@Override
protected DittoRuntimeExceptionBuilder newNotAccessibleExceptionBuilder() {
return ConnectionNotAccessibleException.newBuilder(entityId);
}
@Override
protected void publishEvent(@Nullable final Connection previousEntity, final ConnectivityEvent event) {
if (event instanceof ConnectionDeleted) {
pubSubMediator.tell(DistPubSubAccess.publish(ConnectionDeleted.TYPE, event), getSelf());
} else if (event instanceof ConnectionCreated) {
pubSubMediator.tell(DistPubSubAccess.publish(ConnectionCreated.TYPE, event), getSelf());
}
// no other events are emitted
}
@Override
protected JsonSchemaVersion getEntitySchemaVersion(final Connection entity) {
return entity.getImplementedSchemaVersion();
}
@Override
protected boolean shouldSendResponse(final DittoHeaders dittoHeaders) {
return dittoHeaders.isResponseRequired();
}
@Override
protected boolean isEntityAlwaysAlive() {
return isDesiredStateOpen();
}
@Override
public void postStop() throws Exception {
log.info("stopped connection <{}>", entityId);
super.postStop();
}
@Override
protected void recoveryCompleted(final RecoveryCompleted event) {
log.info("Connection <{}> was recovered: {}", entityId, entity);
recoveredAt = Instant.now();
if (entity != null && entity.getLifecycle().isEmpty()) {
entity = entity.toBuilder().lifecycle(ConnectionLifecycle.ACTIVE).build();
}
if (isDesiredStateOpen()) {
log.debug("Opening connection <{}> after recovery.", entityId);
restoreOpenConnection();
}
super.recoveryCompleted(event);
}
@Override
protected ConnectivityEvent modifyEventBeforePersist(final ConnectivityEvent event) {
final ConnectivityEvent superEvent = super.modifyEventBeforePersist(event);
final Set tags = journalTags(event);
final DittoHeaders headersWithJournalTags = superEvent.getDittoHeaders().toBuilder()
.journalTags(tags)
.build();
log.withCorrelationId(event)
.info("Appending the following tags to event of type <{}> for connection with ID <{}>: <{}>",
event.getType(), event.getEntityId(), tags);
return superEvent.setDittoHeaders(headersWithJournalTags);
}
private Set journalTags() {
final Collection connectionTags = connectionTags();
final Collection activeConnectionTags = activeConnectionTags();
return Stream.concat(connectionTags.stream(), activeConnectionTags.stream()).collect(Collectors.toSet());
}
private Set journalTags(final ConnectivityEvent event) {
final Collection connectionTags = connectionTags(event);
final Collection activeConnectionTags = activeConnectionTags(event);
return Stream.concat(connectionTags.stream(), activeConnectionTags.stream()).collect(Collectors.toSet());
}
private Collection connectionTags() {
return Optional.ofNullable(entity)
.map(Connection::getTags)
.map(Collection::stream)
.map(Stream::toList)
.orElseGet(List::of);
}
private Collection connectionTags(final ConnectivityEvent event) {
final Collection connectionTags;
if (event instanceof ConnectionCreated connectionCreated) {
connectionTags = connectionCreated.getConnection().getTags();
} else if (event instanceof ConnectionModified connectionModified) {
connectionTags = connectionModified.getConnection().getTags();
} else if (event instanceof ConnectionDeleted) {
connectionTags = Set.of();
} else {
connectionTags = connectionTags();
}
return connectionTags;
}
private Collection activeConnectionTags() {
final Set activeConnectionTags;
if (isDesiredStateOpen()) {
activeConnectionTags = Set.of(JOURNAL_TAG_ALWAYS_ALIVE,
MongoReadJournal.PRIORITY_TAG_PREFIX + Optional.ofNullable(priority).orElse(0));
} else {
activeConnectionTags = Set.of();
}
return activeConnectionTags;
}
private Collection activeConnectionTags(final ConnectivityEvent event) {
final Set activeConnectionTags;
final ConnectivityStatus targetConnectionStatus;
if (event instanceof ConnectionCreated connectionCreated) {
targetConnectionStatus = connectionCreated.getConnection().getConnectionStatus();
} else if (event instanceof ConnectionModified connectionModified) {
targetConnectionStatus = connectionModified.getConnection().getConnectionStatus();
} else if (event instanceof ConnectionDeleted) {
targetConnectionStatus = ConnectivityStatus.CLOSED;
} else if (event instanceof ConnectionOpened) {
targetConnectionStatus = ConnectivityStatus.OPEN;
} else if (event instanceof ConnectionClosed) {
targetConnectionStatus = ConnectivityStatus.CLOSED;
} else if (null != entity) {
targetConnectionStatus = entity.getConnectionStatus();
} else {
targetConnectionStatus = ConnectivityStatus.UNKNOWN;
}
final var alwaysAlive = (targetConnectionStatus == ConnectivityStatus.OPEN);
if (alwaysAlive) {
activeConnectionTags = Set.of(JOURNAL_TAG_ALWAYS_ALIVE,
MongoReadJournal.PRIORITY_TAG_PREFIX + Optional.ofNullable(priority).orElse(0));
} else {
activeConnectionTags = Set.of();
}
return activeConnectionTags;
}
@Override
protected void processPingCommand(final PingCommand ping) {
super.processPingCommand(ping);
final String journalTag = ping.getPayload()
.filter(JsonValue::isString)
.map(JsonValue::asString)
.orElse(null);
if (journalTag != null && journalTag.isEmpty() && isDesiredStateOpen()) {
// persistence actor was sent a "ping" with empty journal tag:
// build in adding the "always-alive" tag here by persisting an "empty" event which is just tagged to be
// "always alive". Stop persisting the empty event once every open connection has a tagged event, when
// the persistence ping actor will have a non-empty journal tag configured.
final DittoHeaders eventHeaders = ((DittoHeadersBuilder) DittoHeaders.newBuilder())
.correlationId(ping.getCorrelationId().orElse(null))
.journalTags(journalTags())
.build();
final EmptyEvent emptyEvent =
new EmptyEvent(EmptyEvent.EFFECT_ALWAYS_ALIVE, getRevisionNumber() + 1, eventHeaders);
getSelf().tell(new PersistEmptyEvent(emptyEvent), ActorRef.noSender());
}
if (null != recoveredAt && recoveredAt.plus(GRACE_PERIOD_NOT_RETRIEVING_CONNECTION_STATUS_AFTER_RECOVERY)
.isBefore(Instant.now())) {
// only ask for connection status after initial recovery was completed + some grace period
askSelfForRetrieveConnectionStatus(ping.getCorrelationId().orElse(null));
}
// refresh client actors
if (isDesiredStateOpen()) {
startClientActors(getClientCount(), DittoHeaders.empty());
}
}
private void askSelfForRetrieveConnectionStatus(@Nullable final CharSequence correlationId) {
final var retrieveConnectionStatus = RetrieveConnectionStatus.of(entityId,
DittoHeaders.newBuilder().correlationId(correlationId).build());
Patterns.ask(getSelf(), retrieveConnectionStatus, SELF_RETRIEVE_CONNECTION_STATUS_TIMEOUT)
.whenComplete((response, throwable) -> {
if (response instanceof RetrieveConnectionStatusResponse rcsResp) {
final ConnectivityStatus liveStatus = rcsResp.getLiveStatus();
final String connectionType = Optional.ofNullable(entity)
.map(Connection::getConnectionType).map(Object::toString).orElse("?");
DittoMetrics.counter("connection_live_status_reported")
.tag("connectionId", String.valueOf(entityId))
.tag("connectionType", connectionType)
.tag("status", liveStatus.getName())
.increment();
final DittoDiagnosticLoggingAdapter l = log
.withMdcEntries(
ConnectivityMdcEntryKey.CONNECTION_ID, entityId,
ConnectivityMdcEntryKey.CONNECTION_TYPE, connectionType,
CommonMdcEntryKey.DITTO_LOG_TAG,
"connection-live-status-" + liveStatus.getName()
)
.withCorrelationId(rcsResp);
final String template = "Calculated <{}> live ConnectionStatus: <{}>";
if (liveStatus == ConnectivityStatus.FAILED) {
l.error(template, liveStatus, rcsResp);
} else if (liveStatus == ConnectivityStatus.MISCONFIGURED) {
l.info(template, liveStatus, rcsResp);
} else {
l.info(template, liveStatus, rcsResp);
}
} else if (null != throwable) {
log.withCorrelationId(correlationId)
.warning("Received throwable while waiting for RetrieveConnectionStatusResponse: " +
"<{}: {}>", throwable.getClass().getSimpleName(), throwable.getMessage());
} else if (null != response) {
log.withCorrelationId(correlationId)
.warning("Unknown response while waiting for RetrieveConnectionStatusResponse: " +
"<{}: {}>", response.getClass().getSimpleName(), response);
}
});
}
@Override
public void onMutation(final Command command, final ConnectivityEvent event,
final WithDittoHeaders response, final boolean becomeCreated, final boolean becomeDeleted) {
if (command instanceof StagedCommand stagedCommand) {
++ongoingStagedCommands;
interpretStagedCommand(stagedCommand.withSenderUnlessDefined(getSender()));
} else {
super.onMutation(command, event, response, becomeCreated, becomeDeleted);
}
}
@Override
protected void checkForActivity(final CheckForActivity trigger) {
if (isDesiredStateOpen()) {
// stay in memory forever if desired state is open. check again later in case connection becomes closed.
scheduleCheckForActivity(getActivityCheckConfig().getInactiveInterval());
} else {
super.checkForActivity(trigger);
}
}
/**
* Carry out the actions in a staged command. Synchronous actions are performed immediately followed by recursion
* onto the next action. Asynchronous action are scheduled with the sending of the next staged command at the end.
* Failures abort all asynchronous actions except OPEN_CONNECTION_IGNORE_ERRORS.
*
* @param command the staged command.
*/
private void interpretStagedCommand(final StagedCommand command) {
if (!command.hasNext()) {
// execution complete
--ongoingStagedCommands;
if (inCoordinatedShutdown && ongoingStagedCommands == 0) {
log.debug("Stopping after waiting for ongoing staged commands");
getContext().stop(getSelf());
}
return;
}
switch (command.nextAction()) {
case TEST_CONNECTION -> testConnection(command.next());
case APPLY_EVENT -> command.getEvent().ifPresentOrElse(event -> {
entity = getEventStrategy().handle(event, entity, getRevisionNumber());
interpretStagedCommand(command.next());
},
() -> log.error(
"Failed to handle staged command because required event wasn't present: <{}>",
command));
case PERSIST_AND_APPLY_EVENT -> command.getEvent().ifPresentOrElse(
event -> persistAndApplyEvent(event,
(unusedEvent, connection) -> interpretStagedCommand(command.next())),
() -> log.error("Failed to handle staged command because required event wasn't present: <{}>",
command));
case SEND_RESPONSE -> {
command.getSender().tell(command.getResponse(), getSelf());
interpretStagedCommand(command.next());
}
case PASSIVATE -> {
// This actor will stop. Subsequent actions are ignored.
log.debug("Passivating");
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
passivate();
}
case OPEN_CONNECTION -> openConnection(command.next(), false);
case OPEN_CONNECTION_IGNORE_ERRORS -> openConnection(command.next(), true);
case CLOSE_CONNECTION -> closeConnection(command.next());
case STOP_CLIENT_ACTORS -> {
stopClientActors();
interpretStagedCommand(command.next());
}
case BECOME_CREATED -> {
becomeCreatedHandler();
interpretStagedCommand(command.next());
}
case BECOME_DELETED -> {
becomeDeletedHandler();
interpretStagedCommand(command.next());
}
case CHECK_LOGGING_ENABLED -> checkLoggingEnabled(command.next());
case BROADCAST_TO_CLIENT_ACTORS_IF_STARTED -> {
broadcastToClientActors(command.getCommand(), getSelf());
interpretStagedCommand(command.next());
}
case RETRIEVE_CONNECTION_LOGS -> {
retrieveConnectionLogs((RetrieveConnectionLogs) command.getCommand(), command.getSender());
interpretStagedCommand(command.next());
}
case RETRIEVE_CONNECTION_STATUS -> {
retrieveConnectionStatus((RetrieveConnectionStatus) command.getCommand(), command.getSender());
interpretStagedCommand(command.next());
}
case RETRIEVE_CONNECTION_METRICS -> {
retrieveConnectionMetrics((RetrieveConnectionMetrics) command.getCommand(), command.getSender());
interpretStagedCommand(command.next());
}
case ENABLE_LOGGING -> {
loggingEnabled();
interpretStagedCommand(command.next());
}
case DISABLE_LOGGING -> {
loggingDisabled();
interpretStagedCommand(command.next());
}
default -> log.error("Failed to handle staged command: <{}>", command);
}
}
@Override
protected Receive matchAnyAfterInitialization() {
return ReceiveBuilder.create()
// CreateSubscription is a ThingSearchCommand, but it is created in InboundDispatchingSink from an
// adaptable and directly sent to this actor:
.match(CreateSubscription.class, this::startThingSearchSession)
.matchEquals(Control.CHECK_LOGGING_ACTIVE, this::checkLoggingEnabled)
.matchEquals(Control.TRIGGER_UPDATE_PRIORITY, this::triggerUpdatePriority)
.match(UpdatePriority.class, this::updatePriority)
.match(StopShardedActor.class, this::stopShardedActor)
.match(SudoRetrieveClientActorProps.class, this::retrieveClientActorProps)
// Respond with not-accessible-exception for unhandled connectivity commands
.match(ConnectivitySudoCommand.class, this::notAccessible)
.match(ConnectivityCommand.class, this::notAccessible)
.build()
.orElse(super.matchAnyAfterInitialization());
}
@Override
protected void becomeDeletedHandler() {
cancelPeriodicPriorityUpdate();
super.becomeDeletedHandler();
}
/**
* Route search commands according to subscription ID prefix. This is necessary so that for connections with
* client count > 1, all commands related to 1 search session are routed to the same client actor. This is achieved
* by using a prefix of fixed length of subscription IDs as the hash key of search commands. The length of the
* prefix depends on the client count configured in the connection; it is 1 for connections with client count <= 15.
*
* For the search protocol, all incoming messages are commands (ThingSearchCommand) and all outgoing messages are
* events (SubscriptionEvent).
*
* Message path for incoming search commands:
*
* ConsumerActor -> MessageMappingProcessorActor -> ConnectionPersistenceActor -> ClientActor -> SubscriptionManager
*
* Message path for outgoing search events:
*
* SubscriptionActor -> MessageMappingProcessorActor -> PublisherActor
*
*
* @param command the command to route.
*/
private void startThingSearchSession(final CreateSubscription command) {
if (entity == null) {
logDroppedSignal(command, command.getType(), "No Connection configuration available.");
return;
}
log.debug("Forwarding <{}> to client actors.", command);
// compute the next prefix according to subscriptionCounter and the currently configured client actor count
// ignore any "prefix" field from the command
augmentWithPrefixAndForward(command, entity.getClientCount());
}
private void augmentWithPrefixAndForward(final CreateSubscription createSubscription, final int clientCount) {
subscriptionCounter = (subscriptionCounter + 1) % Math.max(1, clientCount);
final var prefix = getPrefix(getSubscriptionPrefixLength(clientCount), subscriptionCounter);
final var commandWithPrefix = createSubscription.setPrefix(prefix);
connectionPubSub.publishSignal(commandWithPrefix, entityId, prefix, ActorRef.noSender());
}
private static String getPrefix(final int prefixLength, final int subscriptionCounter) {
final var prefixPattern = MessageFormat.format("%0{0,number}X", prefixLength);
return String.format(prefixPattern, subscriptionCounter);
}
private void checkLoggingEnabled(final Control message) {
final CheckConnectionLogsActive checkLoggingActive = CheckConnectionLogsActive.of(entityId, Instant.now());
broadcastToClientActors(checkLoggingActive, getSelf());
}
private void triggerUpdatePriority(final Control message) {
if (entity != null) {
final String correlationId = UUID.randomUUID().toString();
connectionPriorityProvider.getPriorityFor(entityId, correlationId)
.whenComplete((desiredPriority, error) -> {
if (error != null) {
log.withCorrelationId(correlationId)
.warning("Got error when trying to get the desired priority for connection <{}>.",
entityId);
} else {
final DittoHeaders headers = DittoHeaders.newBuilder().correlationId(correlationId).build();
self().tell(new UpdatePriority(desiredPriority, headers), ActorRef.noSender());
}
});
}
}
private void updatePriority(final UpdatePriority updatePriority) {
final Integer desiredPriority = updatePriority.getDesiredPriority();
if (!desiredPriority.equals(priority)) {
log.withCorrelationId(updatePriority)
.info("Updating priority of connection <{}> from <{}> to <{}>", entityId, priority,
desiredPriority);
priority = desiredPriority;
final DittoHeaders headersWithJournalTags =
updatePriority.getDittoHeaders().toBuilder().journalTags(journalTags()).build();
final EmptyEvent emptyEvent = new EmptyEvent(EmptyEvent.EFFECT_PRIORITY_UPDATE, getRevisionNumber() + 1,
headersWithJournalTags);
getSelf().tell(new PersistEmptyEvent(emptyEvent), ActorRef.noSender());
}
}
private void checkLoggingEnabled(final StagedCommand command) {
if (isDesiredStateOpen()) {
startEnabledLoggingChecker();
updateLoggingIfEnabled();
}
interpretStagedCommand(command);
}
private void testConnection(final StagedCommand command) {
final ActorRef origin = command.getSender();
final ActorRef self = getSelf();
final DittoHeaders headersWithDryRun = command.getDittoHeaders()
.toBuilder()
.dryRun(true)
.build();
final TestConnection testConnection = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun);
if (connectionOpened) {
// connection is open, so either another TestConnection command is currently executed or the
// connection has been created in the meantime. In either case reject the new TestConnection command to
// prevent strange behavior.
origin.tell(TestConnectionResponse.alreadyCreated(entityId, command.getDittoHeaders()), self);
} else {
connectionOpened = true;
// no need to start more than 1 client for tests
// set connection status to CLOSED so that client actors will not try to connect on startup
setConnectionStatusClosedForTestConnection();
startAndAskClientActorForTest(testConnection)
.thenAccept(response -> self.tell(
command.withResponse(TestConnectionResponse.success(testConnection.getEntityId(),
response.toString(), command.getDittoHeaders())),
ActorRef.noSender()))
.exceptionally(error -> {
self.tell(
command.withResponse(
toDittoRuntimeException(error, entityId, command.getDittoHeaders())),
ActorRef.noSender());
return null;
});
}
}
private void setConnectionStatusClosedForTestConnection() {
if (entity != null) {
entity = entity.toBuilder().connectionStatus(ConnectivityStatus.CLOSED).build();
}
}
private void openConnection(final StagedCommand command, final boolean ignoreErrors) {
final OpenConnection openConnection = OpenConnection.of(entityId, command.getDittoHeaders());
final Consumer