com.hivemq.mqtt.handler.subscribe.retained.RetainedMessagesSender Maven / Gradle / Ivy
Show all versions of hivemq-community-edition-embedded Show documentation
/*
* Copyright 2019-present HiveMQ GmbH
*
* 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.
*/
package com.hivemq.mqtt.handler.subscribe.retained;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.ImmutableIntArray;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.hivemq.bootstrap.ClientConnection;
import com.hivemq.configuration.HivemqId;
import com.hivemq.configuration.service.MqttConfigurationService;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.annotations.Nullable;
import com.hivemq.mqtt.handler.publish.PublishStatus;
import com.hivemq.mqtt.handler.publish.PublishWriteFailedListener;
import com.hivemq.mqtt.message.QoS;
import com.hivemq.mqtt.message.pool.FreePacketIdRanges;
import com.hivemq.mqtt.message.publish.PUBLISH;
import com.hivemq.mqtt.message.publish.PUBLISHFactory;
import com.hivemq.mqtt.message.publish.PublishWithFuture;
import com.hivemq.mqtt.message.subscribe.Topic;
import com.hivemq.persistence.RetainedMessage;
import com.hivemq.persistence.clientqueue.ClientQueuePersistence;
import com.hivemq.persistence.retained.RetainedMessagePersistence;
import com.hivemq.persistence.util.FutureUtils;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
/**
* Utility methods for using retained messages
*/
@Singleton
public class RetainedMessagesSender {
private static final Logger log = LoggerFactory.getLogger(RetainedMessagesSender.class);
private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();
static {
CLOSED_CHANNEL_EXCEPTION.setStackTrace(new StackTraceElement[0]);
}
private final @NotNull HivemqId hiveMQId;
private final @NotNull RetainedMessagePersistence retainedMessagePersistence;
private final @NotNull ClientQueuePersistence clientQueuePersistence;
private final @NotNull MqttConfigurationService mqttConfigurationService;
@Inject
public RetainedMessagesSender(
final @NotNull HivemqId hiveMQId,
final @NotNull RetainedMessagePersistence retainedMessagePersistence,
final @NotNull ClientQueuePersistence clientQueuePersistence,
final @NotNull MqttConfigurationService mqttConfigurationService) {
this.hiveMQId = hiveMQId;
this.retainedMessagePersistence = retainedMessagePersistence;
this.clientQueuePersistence = clientQueuePersistence;
this.mqttConfigurationService = mqttConfigurationService;
}
/**
* Writes out the retained message for a given topic to the given {@link io.netty.channel.Channel}
*
*
* @param subscribedTopics the topic to get the retained message for. Must not include wildcards.
* @param channel the channel to write the retained message to.
*/
public @NotNull ListenableFuture writeRetainedMessages(
final @NotNull Channel channel, final @Nullable Topic... subscribedTopics) {
if (subscribedTopics == null) {
return Futures.immediateFuture(null);
}
final String clientId = ClientConnection.of(channel).getClientId();
final ImmutableList.Builder> retainedMessageFutures = ImmutableList.builder();
for (final Topic topic : subscribedTopics) {
retainedMessageFutures.add(retainedMessagePersistence.get(topic.getTopic()));
}
// Futures.allAsList preserves the original order
final ListenableFuture> retainedMessagesFuture =
Futures.allAsList(retainedMessageFutures.build());
final SettableFuture resultFuture = SettableFuture.create();
Futures.addCallback(retainedMessagesFuture,
new SendRetainedMessageCallback(subscribedTopics,
hiveMQId,
clientId,
resultFuture,
channel,
clientQueuePersistence,
mqttConfigurationService),
channel.eventLoop());
return resultFuture;
}
private static class SendRetainedMessageCallback implements FutureCallback> {
private final @NotNull Topic[] subscribedTopics;
private final @NotNull HivemqId hivemqId;
private final @NotNull String clientId;
private final @NotNull SettableFuture resultFuture;
private final @NotNull Channel channel;
private final @NotNull ClientQueuePersistence clientQueuePersistence;
private final @NotNull MqttConfigurationService mqttConfigurationService;
SendRetainedMessageCallback(
final @NotNull Topic[] subscribedTopics,
final @NotNull HivemqId hivemqId,
final @NotNull String clientId,
final @NotNull SettableFuture resultFuture,
final @NotNull Channel channel,
final @NotNull ClientQueuePersistence clientQueuePersistence,
final @NotNull MqttConfigurationService mqttConfigurationService) {
this.subscribedTopics = subscribedTopics;
this.hivemqId = hivemqId;
this.clientId = clientId;
this.resultFuture = resultFuture;
this.channel = channel;
this.clientQueuePersistence = clientQueuePersistence;
this.mqttConfigurationService = mqttConfigurationService;
}
@Override
public void onSuccess(final List retainedMessages) {
final ImmutableList.Builder builder = ImmutableList.builder();
for (int i = 0; i < retainedMessages.size(); i++) {
final RetainedMessage retainedMessage = retainedMessages.get(i);
//list can contain null entries
if (retainedMessage == null) {
continue;
}
// Topics and retained messages have the same order
final Topic subscribedTopic = subscribedTopics[i];
final QoS qos = QoS.getMinQoS(subscribedTopic.getQoS(), retainedMessage.getQos());
final ImmutableIntArray subscriptionIdentifiers;
if (subscribedTopic.getSubscriptionIdentifier() != null) {
subscriptionIdentifiers = ImmutableIntArray.of(subscribedTopic.getSubscriptionIdentifier());
} else {
subscriptionIdentifiers = ImmutableIntArray.of();
}
final PUBLISHFactory.Mqtt5Builder publishBuilder =
new PUBLISHFactory.Mqtt5Builder().withTimestamp(System.currentTimeMillis())
.withHivemqId(hivemqId.get())
.withPayload(retainedMessage.getMessage())
.withPublishId(retainedMessage.getPublishId())
.withMessageExpiryInterval(retainedMessage.getMessageExpiryInterval())
.withTopic(subscribedTopic.getTopic())
.withRetain(true)
.withDuplicateDelivery(false)
.withQoS(qos)
.withOnwardQos(qos)
.withUserProperties(retainedMessage.getUserProperties())
.withResponseTopic(retainedMessage.getResponseTopic())
.withContentType(retainedMessage.getContentType())
.withCorrelationData(retainedMessage.getCorrelationData())
.withPayloadFormatIndicator(retainedMessage.getPayloadFormatIndicator())
.withSubscriptionIdentifiers(subscriptionIdentifiers);
builder.add(publishBuilder.build());
}
sendOutMessages(builder.build());
}
private void sendOutMessages(final @NotNull List retainedPublishes) {
if (!channel.isActive()) {
resultFuture.setException(CLOSED_CHANNEL_EXCEPTION);
return;
}
if (log.isTraceEnabled()) {
for (final Topic topic : subscribedTopics) {
log.trace("Sending retained message with topic [{}] for client [{}]", topic.getTopic(), clientId);
}
}
final ImmutableList.Builder builder = ImmutableList.builder();
final ImmutableList.Builder> futures = ImmutableList.builder();
for (final PUBLISH publish : retainedPublishes) {
if (publish.getQoS() == QoS.AT_MOST_ONCE) {
futures.add(sendQos0PublishDirectly(publish));
continue;
}
builder.add(publish);
}
final ImmutableList qos1and2Messages = builder.build();
if (qos1and2Messages.isEmpty()) {
resultFuture.setFuture(FutureUtils.voidFutureFromList(futures.build()));
return;
}
final Long queueLimit = ClientConnection.of(channel).getQueueSizeMaximum();
futures.add(clientQueuePersistence.add(clientId,
false,
qos1and2Messages,
true,
Objects.requireNonNullElseGet(queueLimit, mqttConfigurationService::maxQueuedMessages)));
resultFuture.setFuture(FutureUtils.voidFutureFromList(futures.build()));
}
private ListenableFuture sendQos0PublishDirectly(final @NotNull PUBLISH qos0Publish) {
final SettableFuture resultFuture = SettableFuture.create();
final SettableFuture publishFuture = SettableFuture.create();
Futures.addCallback(publishFuture, new FutureCallback<>() {
@Override
public void onSuccess(final @Nullable PublishStatus status) {
if (status == PublishStatus.DELIVERED) {
resultFuture.set(null);
} else {
resultFuture.setException(new ClosedChannelException());
}
if (qos0Publish.getPacketIdentifier() != 0) {
final FreePacketIdRanges freePacketIdRanges = ClientConnection.of(channel).getFreePacketIdRanges();
freePacketIdRanges.returnId(qos0Publish.getPacketIdentifier());
}
}
@Override
public void onFailure(final @NotNull Throwable t) {
if (t instanceof CancellationException) {
//ignore because task was cancelled because channel became inactive and
//response has already been sent by callback from ChannelInactiveHandler
return;
}
}
}, MoreExecutors.directExecutor());
final PublishWithFuture message = new PublishWithFuture(qos0Publish, publishFuture, false);
channel.writeAndFlush(message).addListener(new PublishWriteFailedListener(publishFuture));
return resultFuture;
}
@Override
public void onFailure(final @NotNull Throwable throwable) {
resultFuture.setException(throwable);
}
}
}