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

com.hivemq.mqtt.handler.disconnect.DisconnectHandler Maven / Gradle / Ivy

The newest version!
/*
 * 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.disconnect;

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.bootstrap.ClientConnectionContext;
import com.hivemq.bootstrap.ClientState;
import com.hivemq.configuration.service.InternalConfigurations;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.annotations.Nullable;
import com.hivemq.extensions.events.OnClientDisconnectEvent;
import com.hivemq.extensions.packets.general.UserPropertiesImpl;
import com.hivemq.limitation.TopicAliasLimiter;
import com.hivemq.logging.EventLog;
import com.hivemq.metrics.MetricsHolder;
import com.hivemq.mqtt.message.disconnect.DISCONNECT;
import com.hivemq.persistence.clientsession.ClientSessionPersistence;
import com.hivemq.persistence.connection.ConnectionPersistence;
import com.hivemq.util.Checkpoints;
import com.hivemq.util.Exceptions;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;

import static com.hivemq.mqtt.message.reason.Mqtt5DisconnectReasonCode.NORMAL_DISCONNECTION;

@Singleton
@ChannelHandler.Sharable
public class DisconnectHandler extends SimpleChannelInboundHandler {

    private static final Logger log = LoggerFactory.getLogger(DisconnectHandler.class);

    private final @NotNull EventLog eventLog;
    private final @NotNull MetricsHolder metricsHolder;
    private final @NotNull TopicAliasLimiter topicAliasLimiter;
    private final @NotNull ClientSessionPersistence clientSessionPersistence;
    private final @NotNull ConnectionPersistence connectionPersistence;

    private final boolean logClientReasonString;

    @Inject
    public DisconnectHandler(
            final @NotNull EventLog eventLog,
            final @NotNull MetricsHolder metricsHolder,
            final @NotNull TopicAliasLimiter topicAliasLimiter,
            final @NotNull ClientSessionPersistence clientSessionPersistence,
            final @NotNull ConnectionPersistence connectionPersistence) {
        this.eventLog = eventLog;
        this.metricsHolder = metricsHolder;
        this.topicAliasLimiter = topicAliasLimiter;
        this.clientSessionPersistence = clientSessionPersistence;
        this.connectionPersistence = connectionPersistence;
        logClientReasonString = InternalConfigurations.LOG_CLIENT_REASON_STRING_ON_DISCONNECT_ENABLED;
    }

    @Override
    protected void channelRead0(
            final @NotNull ChannelHandlerContext ctx, final @NotNull DISCONNECT msg) throws Exception {

        final ClientConnectionContext clientConnectionContext = ClientConnectionContext.of(ctx.channel());

        clientConnectionContext.proposeClientState(ClientState.DISCONNECTING);

        final String clientId = clientConnectionContext.getClientId();

        //no version check necessary, because mqtt 3 disconnect session expiry interval = SESSION_EXPIRY_NOT_SET
        if (msg.getSessionExpiryInterval() != DISCONNECT.SESSION_EXPIRY_NOT_SET) {
            clientConnectionContext.setClientSessionExpiryInterval(msg.getSessionExpiryInterval());
        }

        if (log.isTraceEnabled()) {
            log.trace("The client [{}] sent a disconnect message.", clientId);
        }
        eventLog.clientDisconnectedGracefully(clientConnectionContext,
                logClientReasonString ? msg.getReasonString() : null);

        clientConnectionContext.setSendWill(msg.getReasonCode() != NORMAL_DISCONNECTION);

        ctx.pipeline()
                .fireUserEventTriggered(new OnClientDisconnectEvent(msg.getReasonCode().toDisconnectedReasonCode(),
                        msg.getReasonString(),
                        UserPropertiesImpl.of(msg.getUserProperties().asList()),
                        true));

        clientConnectionContext.proposeClientState(ClientState.DISCONNECTED_BY_CLIENT);
        ctx.channel().close();
    }

    @Override
    public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {

        final ClientConnectionContext clientConnectionContext = ClientConnectionContext.of(ctx.channel());

        // Any disconnect status other than unspecified is already handled.
        // We can be sure that we are logging the initial log and event when we can set this state.
        clientConnectionContext.proposeClientState(ClientState.DISCONNECTED_UNSPECIFIED);

        final ClientState clientState = clientConnectionContext.getClientState();
        final boolean initialDisconnectEvent = clientState == ClientState.DISCONNECTED_UNSPECIFIED;

        //only change the session information if user is authenticated
        persistDisconnectState(clientConnectionContext);

        //increase metrics
        metricsHolder.getClosedConnectionsCounter().inc();

        if (initialDisconnectEvent) {
            eventLog.clientDisconnectedUngracefully(clientConnectionContext);
            ctx.pipeline().fireUserEventTriggered(new OnClientDisconnectEvent(null, null, null, false));
        }

        final String[] topicAliasMapping = clientConnectionContext.getTopicAliasMapping();
        if (topicAliasMapping != null) {
            topicAliasLimiter.finishUsage(topicAliasMapping);
        }

        super.channelInactive(ctx);
    }

    private void persistDisconnectState(final @NotNull ClientConnectionContext clientConnectionContext) {

        final SettableFuture disconnectFuture = clientConnectionContext.getDisconnectFuture();

        // When the ClientConnectionContext has not the ClientConnection implementation, this client wasn't even
        // persisted in the ConnectionPersistence.
        if (clientConnectionContext.getClientId() == null || !(clientConnectionContext instanceof ClientConnection)) {
            if (disconnectFuture != null) {
                disconnectFuture.set(null);
            }
            return;
        }

        final ClientConnection clientConnection = (ClientConnection) clientConnectionContext;

        // We are not the currently persisted client.
        if (clientConnection != connectionPersistence.get(clientConnection.getClientId())) {
            if (disconnectFuture != null) {
                disconnectFuture.set(null);
            }
            return;
        }

        if (clientConnection.isPreventLwt()) {
            clientConnection.setSendWill(false);

            // ungraceful disconnect
        } else if ((clientConnection.getClientState() == ClientState.DISCONNECTED_BY_SERVER) ||
                (clientConnection.getClientState() == ClientState.DISCONNECTED_UNSPECIFIED)) {
            clientConnection.setSendWill(true);
        }

        final ListenableFuture persistenceFuture =
                clientSessionPersistence.clientDisconnected(clientConnection.getClientId(),
                        clientConnection.isSendWill(),
                        clientConnection.getClientSessionExpiryInterval());
        Futures.addCallback(persistenceFuture, new FutureCallback<>() {
            @Override
            public void onSuccess(final @Nullable Void result) {
                connectionPersistence.remove(clientConnection);
                Checkpoints.checkpoint("client-disconnected");
                final SettableFuture disconnectFuture = clientConnection.getDisconnectFuture();
                if (disconnectFuture != null) {
                    disconnectFuture.set(null);
                }
            }

            @Override
            public void onFailure(final @NotNull Throwable throwable) {
                final boolean persistent = clientConnectionContext.getClientSessionExpiryInterval() > 0;
                Exceptions.rethrowError("Unable to update client session data for disconnecting client " +
                        clientConnectionContext.getClientId() +
                        " with clean session set to " +
                        !persistent +
                        ".", throwable);
            }
        }, MoreExecutors.directExecutor());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy