com.digitalpetri.opcua.sdk.client.ClientSessionManager.java Source code

Java tutorial

Introduction

Here is the source code for com.digitalpetri.opcua.sdk.client.ClientSessionManager.java

Source

/*
 * Copyright 2016 Kevin Herron
 *
 * 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.digitalpetri.opcua.sdk.client;

import java.nio.ByteBuffer;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

import com.codepoetics.protonpack.StreamUtils;
import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaSubscription;
import com.digitalpetri.opcua.sdk.client.subscriptions.OpcUaSubscriptionManager;
import com.digitalpetri.opcua.stack.client.UaTcpStackClient;
import com.digitalpetri.opcua.stack.core.StatusCodes;
import com.digitalpetri.opcua.stack.core.UaException;
import com.digitalpetri.opcua.stack.core.channel.ClientSecureChannel;
import com.digitalpetri.opcua.stack.core.security.SecurityAlgorithm;
import com.digitalpetri.opcua.stack.core.security.SecurityPolicy;
import com.digitalpetri.opcua.stack.core.types.builtin.ByteString;
import com.digitalpetri.opcua.stack.core.types.builtin.DateTime;
import com.digitalpetri.opcua.stack.core.types.builtin.ExtensionObject;
import com.digitalpetri.opcua.stack.core.types.builtin.StatusCode;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UInteger;
import com.digitalpetri.opcua.stack.core.types.structured.ActivateSessionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.ActivateSessionResponse;
import com.digitalpetri.opcua.stack.core.types.structured.CloseSessionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CloseSessionResponse;
import com.digitalpetri.opcua.stack.core.types.structured.CreateSessionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CreateSessionResponse;
import com.digitalpetri.opcua.stack.core.types.structured.EndpointDescription;
import com.digitalpetri.opcua.stack.core.types.structured.RequestHeader;
import com.digitalpetri.opcua.stack.core.types.structured.SignatureData;
import com.digitalpetri.opcua.stack.core.types.structured.SignedSoftwareCertificate;
import com.digitalpetri.opcua.stack.core.types.structured.TransferResult;
import com.digitalpetri.opcua.stack.core.types.structured.TransferSubscriptionsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.TransferSubscriptionsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.UserIdentityToken;
import com.digitalpetri.opcua.stack.core.util.NonceUtil;
import com.digitalpetri.opcua.stack.core.util.SignatureUtil;
import com.google.common.collect.ImmutableList;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.jooq.lambda.tuple.Tuple2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
import static com.google.common.collect.Lists.newCopyOnWriteArrayList;
import static java.util.concurrent.CompletableFuture.completedFuture;

class ClientSessionManager {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final List<SessionActivityListener> listeners = newCopyOnWriteArrayList();

    private final AtomicReference<State> state = new AtomicReference<>(new Inactive());

    private final OpcUaClient client;

    ClientSessionManager(OpcUaClient client) {
        this.client = client;

        Predicate<StatusCode> sessionError = statusCode -> {
            long status = statusCode.getValue();

            return status == StatusCodes.Bad_SessionClosed || status == StatusCodes.Bad_SessionIdInvalid
                    || status == StatusCodes.Bad_SessionNotActivated;
        };

        Predicate<StatusCode> secureChannelError = statusCode -> {
            long status = statusCode.getValue();

            return status == StatusCodes.Bad_SecureChannelIdInvalid
                    || status == StatusCodes.Bad_SecurityChecksFailed
                    || status == StatusCodes.Bad_TcpSecureChannelUnknown;
        };

        client.addFaultListener(serviceFault -> {
            StatusCode serviceResult = serviceFault.getResponseHeader().getServiceResult();

            if (sessionError.or(secureChannelError).test(serviceResult)) {

                logger.debug("ServiceFault: {}", serviceResult);

                State currentState = state.get();

                if (currentState instanceof Active) {
                    Creating creating = new Creating();

                    if (state.compareAndSet(currentState, creating)) {
                        OpcUaSession session = ((Active) currentState).session;

                        notifySessionInactive(session);

                        client.getStackClient().disconnect().whenCompleteAsync((v, ex) -> createSession(creating));
                    }
                }
            }
        });
    }

    void addListener(SessionActivityListener listener) {
        listeners.add(listener);
    }

    void removeListener(SessionActivityListener listener) {
        listeners.remove(listener);
    }

    CompletableFuture<OpcUaSession> getSession() {
        State currentState = state.get();

        logger.trace("getSession(), currentState={}", currentState.getClass().getSimpleName());

        if (currentState instanceof Inactive) {
            Creating creatingState = new Creating();

            if (state.compareAndSet(currentState, creatingState)) {
                CompletableFuture<OpcUaSession> sessionFuture = creatingState.sessionFuture;

                createSession(creatingState);

                return sessionFuture;
            } else {
                return getSession();
            }
        } else if (currentState instanceof Creating) {
            return ((Creating) currentState).sessionFuture;
        } else if (currentState instanceof Activating) {
            return ((Activating) currentState).sessionFuture;
        } else if (currentState instanceof Transferring) {
            return ((Transferring) currentState).sessionFuture;
        } else if (currentState instanceof Active) {
            return ((Active) currentState).sessionFuture;
        } else if (currentState instanceof Reactivating) {
            return ((Reactivating) currentState).sessionFuture;
        } else if (currentState instanceof Closing) {
            CompletableFuture<OpcUaSession> future = new CompletableFuture<>();

            ((Closing) currentState).closeFuture
                    .whenCompleteAsync((oldSession, ex) -> getSession().whenComplete((session, ex2) -> {
                        if (session != null)
                            future.complete(session);
                        else
                            future.completeExceptionally(ex2);
                    }));

            return future;
        } else {
            throw new IllegalStateException("unexpected state: " + currentState.getClass());
        }
    }

    CompletableFuture<Void> closeSession() {
        State currentState = state.get();

        logger.trace("closeSession(), currentState={}", currentState.getClass().getSimpleName());

        if (currentState instanceof Inactive) {
            return completedFuture(null);
        } else if (currentState instanceof Closing) {
            return ((Closing) currentState).closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
        } else if (currentState instanceof Creating) {
            Closing closingState = new Closing();

            if (state.compareAndSet(currentState, closingState)) {
                closeSession(closingState, ((Creating) currentState).sessionFuture);

                return closingState.closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
            } else {
                return closeSession();
            }
        } else if (currentState instanceof Activating) {
            Closing closingState = new Closing();

            if (state.compareAndSet(currentState, closingState)) {
                closeSession(closingState, ((Activating) currentState).sessionFuture);

                return closingState.closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
            } else {
                return closeSession();
            }
        } else if (currentState instanceof Reactivating) {
            Closing closingState = new Closing();

            if (state.compareAndSet(currentState, closingState)) {
                closeSession(closingState, ((Reactivating) currentState).sessionFuture);

                return closingState.closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
            } else {
                return closeSession();
            }
        } else if (currentState instanceof Transferring) {
            Closing closingState = new Closing();

            if (state.compareAndSet(currentState, closingState)) {
                closeSession(closingState, ((Transferring) currentState).sessionFuture);

                return closingState.closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
            } else {
                return closeSession();
            }
        } else if (currentState instanceof Active) {
            Closing closingState = new Closing();

            if (state.compareAndSet(currentState, closingState)) {
                closeSession(closingState, ((Active) currentState).sessionFuture);

                return closingState.closeFuture.thenApply(s -> (Void) null).exceptionally(ex -> null);
            } else {
                return closeSession();
            }
        } else {
            throw new IllegalStateException("unexpected state: " + currentState.getClass());
        }
    }

    private void notifySessionActive(OpcUaSession session) {
        listeners.forEach(listener -> {
            try {
                listener.onSessionActive(session);
            } catch (Throwable t) {
                logger.warn("Uncaught Throwable notifying listener: {}", listener, t);
            }
        });
    }

    private void notifySessionInactive(OpcUaSession session) {
        listeners.forEach(listener -> {
            try {
                listener.onSessionInactive(session);
            } catch (Throwable t) {
                logger.warn("Uncaught Throwable notifying listener: {}", listener, t);
            }
        });
    }

    private void createSession(Creating creatingState) {
        UaTcpStackClient stackClient = client.getStackClient();

        String serverUri = stackClient.getEndpoint().flatMap(e -> {
            String gatewayServerUri = e.getServer().getGatewayServerUri();
            if (gatewayServerUri != null && !gatewayServerUri.isEmpty()) {
                return Optional.ofNullable(e.getServer().getApplicationUri());
            } else {
                return Optional.empty();
            }
        }).orElse(null);

        ByteString clientNonce = NonceUtil.generateNonce(32);

        ByteString clientCertificate = stackClient.getConfig().getCertificate().map(c -> {
            try {
                return ByteString.of(c.getEncoded());
            } catch (CertificateEncodingException e) {
                return ByteString.NULL_VALUE;
            }
        }).orElse(ByteString.NULL_VALUE);

        CreateSessionRequest request = new CreateSessionRequest(client.newRequestHeader(),
                stackClient.getApplication(), serverUri, stackClient.getEndpointUrl(),
                client.getConfig().getSessionName().get(), clientNonce, clientCertificate,
                client.getConfig().getSessionTimeout().doubleValue(),
                client.getConfig().getMaxResponseMessageSize());

        logger.debug("Sending CreateSessionRequest...");

        stackClient.<CreateSessionResponse>sendRequest(request).whenCompleteAsync((csr, ex) -> {
            CompletableFuture<OpcUaSession> sessionFuture = creatingState.sessionFuture;

            if (csr != null) {
                logger.debug("Session created: {}", csr.getSessionId());

                Activating activatingState = new Activating(sessionFuture);

                if (state.compareAndSet(creatingState, activatingState)) {
                    activateSession(activatingState, csr);
                }
            } else {
                logger.debug("CreateSession failed: {}", ex.getMessage(), ex);

                state.compareAndSet(creatingState, new Inactive());
                sessionFuture.completeExceptionally(ex);
            }
        });
    }

    private void activateSession(Activating activatingState, CreateSessionResponse csr) {
        UaTcpStackClient stackClient = client.getStackClient();

        Function<ClientSecureChannel, CompletableFuture<ActivateSessionResponse>> activate = secureChannel -> {
            try {
                Channel channel = secureChannel.getChannel();

                channel.pipeline().addLast(new InactivityHandler());

                EndpointDescription endpoint = stackClient.getEndpoint()
                        .orElseThrow(() -> new Exception("cannot create session with no endpoint configured"));

                Tuple2<UserIdentityToken, SignatureData> tuple = client.getConfig().getIdentityProvider()
                        .getIdentityToken(endpoint, csr.getServerNonce());

                UserIdentityToken userIdentityToken = tuple.v1();
                SignatureData userTokenSignature = tuple.v2();

                ActivateSessionRequest request = new ActivateSessionRequest(
                        client.newRequestHeader(csr.getAuthenticationToken()),
                        buildClientSignature(secureChannel, csr), new SignedSoftwareCertificate[0], new String[0],
                        ExtensionObject.encode(userIdentityToken), userTokenSignature);

                logger.debug("Sending ActivateSessionRequest, secureChannelId={}, channel={}...",
                        secureChannel.getChannelId(), secureChannel.getChannel());

                return stackClient.sendRequest(request);
            } catch (Exception e) {
                CompletableFuture<ActivateSessionResponse> f = new CompletableFuture<>();
                f.completeExceptionally(e);
                return f;
            }
        };

        stackClient.getChannelFuture().thenCompose(activate).whenCompleteAsync((asr, ex) -> {
            CompletableFuture<OpcUaSession> sessionFuture = activatingState.sessionFuture;

            if (asr != null) {
                logger.debug("Session activated: {}", csr.getSessionId());

                OpcUaSession session = new OpcUaSession(csr.getAuthenticationToken(), csr.getSessionId(),
                        client.getConfig().getSessionName().get(), csr.getRevisedSessionTimeout(),
                        csr.getMaxRequestMessageSize(), csr.getServerCertificate(),
                        csr.getServerSoftwareCertificates());

                session.setServerNonce(asr.getServerNonce());

                OpcUaSubscriptionManager subscriptionManager = client.getSubscriptionManager();
                int subscriptionCount = subscriptionManager.getSubscriptions().size();
                boolean transferNeeded = subscriptionCount > 0;

                logger.debug("subscriptionCount={}, transferNeeded={}", subscriptionCount, transferNeeded);

                if (transferNeeded) {
                    Transferring transferringState = new Transferring(sessionFuture);

                    if (state.compareAndSet(activatingState, transferringState)) {
                        transferSubscriptions(transferringState, session);
                    }
                } else {
                    state.compareAndSet(activatingState, new Active(session, sessionFuture));
                    sessionFuture.complete(session);
                }
            } else {
                logger.debug("ActivateSession failed: {}", ex.getMessage(), ex);

                state.compareAndSet(activatingState, new Inactive());
                sessionFuture.completeExceptionally(ex);
            }
        });
    }

    private void reactivateSession(Reactivating reactivatingState, OpcUaSession previousSession) {
        UaTcpStackClient stackClient = client.getStackClient();

        Function<ClientSecureChannel, CompletionStage<ActivateSessionResponse>> activate = secureChannel -> {
            try {
                EndpointDescription endpoint = stackClient.getEndpoint()
                        .orElseThrow(() -> new Exception("cannot create session with no endpoint configured"));

                Tuple2<UserIdentityToken, SignatureData> tuple = client.getConfig().getIdentityProvider()
                        .getIdentityToken(endpoint, previousSession.getServerNonce());

                UserIdentityToken userIdentityToken = tuple.v1();
                SignatureData userTokenSignature = tuple.v2();

                SignatureData clientSignature = buildClientSignature(secureChannel,
                        previousSession.getServerCertificate(), previousSession.getServerNonce());

                ActivateSessionRequest request = new ActivateSessionRequest(
                        client.newRequestHeader(previousSession.getAuthenticationToken()), clientSignature,
                        new SignedSoftwareCertificate[0], new String[0], ExtensionObject.encode(userIdentityToken),
                        userTokenSignature);

                logger.debug("Sending (re)ActivateSessionRequest, secureChannelId={}, channel={}...",
                        secureChannel.getChannelId(), secureChannel.getChannel());

                return stackClient.sendRequest(request);
            } catch (Exception e) {
                CompletableFuture<ActivateSessionResponse> f = new CompletableFuture<>();
                f.completeExceptionally(e);
                return f;
            }
        };

        stackClient.getChannelFuture().thenCompose(activate).whenCompleteAsync((asr, ex) -> {
            CompletableFuture<OpcUaSession> sessionFuture = reactivatingState.sessionFuture;

            if (asr != null) {
                logger.debug("Session reactivated: {}", previousSession.getSessionId());

                OpcUaSession newSession = new OpcUaSession(previousSession.getAuthenticationToken(),
                        previousSession.getSessionId(), client.getConfig().getSessionName().get(),
                        previousSession.getSessionTimeout(), previousSession.getMaxRequestSize(),
                        previousSession.getServerCertificate(), previousSession.getServerSoftwareCertificates());

                newSession.setServerNonce(asr.getServerNonce());

                state.compareAndSet(reactivatingState, new Active(newSession, sessionFuture));

                sessionFuture.complete(newSession);
            } else {
                logger.debug("(re)ActivateSession failed: {}", ex.getMessage(), ex);

                StatusCode statusCode = UaException.extract(ex).map(UaException::getStatusCode)
                        .orElse(StatusCode.BAD);

                if (statusCode.getValue() == StatusCodes.Bad_SessionClosed
                        || statusCode.getValue() == StatusCodes.Bad_SessionIdInvalid
                        || statusCode.getValue() == StatusCodes.Bad_SessionNotActivated
                        || statusCode.getValue() == StatusCodes.Bad_SecurityChecksFailed) {

                    // A session-related error means the session is no longer valid.
                    // Create a new session re-using the current future.
                    Creating creating = new Creating(sessionFuture);

                    if (state.compareAndSet(reactivatingState, creating)) {
                        createSession(creating);
                    } else {
                        // We're no longer re-activating for whatever reason (asked to close?).
                        // The only way this sessionFuture can be completed is exceptionally.
                        sessionFuture.completeExceptionally(ex);
                    }
                } else {
                    // A non-session-related error, such as not being connected yet.
                    // Fail the current future and try again.
                    Reactivating reactivatingAgain = new Reactivating();

                    if (state.compareAndSet(reactivatingState, reactivatingAgain)) {
                        reactivateSession(reactivatingAgain, previousSession);
                    }

                    sessionFuture.completeExceptionally(ex);
                }
            }
        });
    }

    private void transferSubscriptions(Transferring transferringState, OpcUaSession session) {
        UaTcpStackClient stackClient = client.getStackClient();
        OpcUaSubscriptionManager subscriptionManager = client.getSubscriptionManager();
        ImmutableList<UaSubscription> subscriptions = subscriptionManager.getSubscriptions();

        UInteger[] subscriptionIdsArray = subscriptions.stream().map(UaSubscription::getSubscriptionId)
                .toArray(UInteger[]::new);

        TransferSubscriptionsRequest request = new TransferSubscriptionsRequest(
                client.newRequestHeader(session.getAuthenticationToken()), subscriptionIdsArray, true);

        logger.debug("Sending TransferSubscriptionsRequest...");

        stackClient.<TransferSubscriptionsResponse>sendRequest(request).whenCompleteAsync((tsr, ex) -> {
            CompletableFuture<OpcUaSession> sessionFuture = transferringState.sessionFuture;

            if (tsr != null) {
                TransferResult[] results = tsr.getResults();

                for (int i = 0; i < results.length; i++) {
                    TransferResult result = results[i];

                    if (!result.getStatusCode().isGood()) {
                        UaSubscription subscription = subscriptions.get(i);

                        subscriptionManager.transferFailed(subscription.getSubscriptionId(),
                                result.getStatusCode());
                    }
                }

                if (logger.isDebugEnabled()) {
                    Stream<UInteger> subscriptionIds = subscriptions.stream()
                            .map(UaSubscription::getSubscriptionId);
                    Stream<StatusCode> statusCodes = Arrays.stream(results).map(TransferResult::getStatusCode);

                    String[] ss = StreamUtils
                            .zip(subscriptionIds, statusCodes,
                                    (i, s) -> String.format("id=%s/%s", i,
                                            StatusCodes.lookup(s.getValue()).map(sa -> sa[0]).orElse(s.toString())))
                            .toArray(String[]::new);

                    logger.debug("TransferSubscriptions results: {}", Arrays.toString(ss));
                }

                state.compareAndSet(transferringState, new Active(session, sessionFuture));
                sessionFuture.complete(session);
            } else {
                StatusCode statusCode = UaException.extract(ex).map(UaException::getStatusCode)
                        .orElse(StatusCode.BAD);

                // Bad_ServiceUnsupported is the correct response when transfers aren't supported but
                // server implementations tend to interpret the spec in their own unique way...
                if (statusCode.getValue() == StatusCodes.Bad_NotImplemented
                        || statusCode.getValue() == StatusCodes.Bad_NotSupported
                        || statusCode.getValue() == StatusCodes.Bad_OutOfService
                        || statusCode.getValue() == StatusCodes.Bad_ServiceUnsupported) {

                    logger.debug("TransferSubscriptions not supported: {}", statusCode);

                    // transferFailed() will remove the subscription, but that is okay
                    // because the list from getSubscriptions() above is a copy.
                    for (UaSubscription subscription : subscriptions) {
                        subscriptionManager.transferFailed(subscription.getSubscriptionId(), statusCode);
                    }

                    state.compareAndSet(transferringState, new Active(session, sessionFuture));
                    sessionFuture.complete(session);
                } else {
                    logger.debug("TransferSubscriptions failed: {}", statusCode);

                    Closing closing = new Closing();

                    if (state.compareAndSet(transferringState, closing)) {
                        closeSession(closing, completedFuture(session));

                        closing.closeFuture.whenComplete((v, ex2) -> sessionFuture.completeExceptionally(ex));
                    }
                }
            }
        });
    }

    private void closeSession(Closing closingState, CompletableFuture<OpcUaSession> sessionFuture) {
        sessionFuture.whenComplete((session, ex) -> {
            if (session != null) {
                UaTcpStackClient stackClient = client.getStackClient();

                RequestHeader requestHeader = new RequestHeader(session.getAuthenticationToken(), DateTime.now(),
                        client.nextRequestHandle(), uint(0), null, uint(5000), null);

                CloseSessionRequest request = new CloseSessionRequest(requestHeader, true);

                logger.debug("Sending CloseSessionRequest...");

                stackClient.<CloseSessionResponse>sendRequest(request).whenCompleteAsync((csr, ex2) -> {
                    if (ex2 != null) {
                        logger.debug("CloseSession failed: {}", ex2.getMessage(), ex2);
                    } else {
                        logger.debug("Session closed: {}", session.getSessionId());
                    }

                    state.compareAndSet(closingState, new Inactive());
                    closingState.closeFuture.complete(session);
                });
            } else {
                state.compareAndSet(closingState, new Inactive());
                closingState.closeFuture.completeExceptionally(ex);
            }
        });
    }

    private SignatureData buildClientSignature(ClientSecureChannel secureChannel, CreateSessionResponse response) {
        ByteString serverCert = response.getServerCertificate() != null ? response.getServerCertificate()
                : ByteString.NULL_VALUE;
        ByteString serverNonce = response.getServerNonce() != null ? response.getServerNonce()
                : ByteString.NULL_VALUE;

        return buildClientSignature(secureChannel, serverCert, serverNonce);
    }

    private SignatureData buildClientSignature(ClientSecureChannel secureChannel, ByteString serverCertificate,
            ByteString serverNonce) {

        byte[] serverNonceBytes = Optional.ofNullable(serverNonce.bytes()).orElse(new byte[0]);
        byte[] serverCertificateBytes = Optional.ofNullable(serverCertificate.bytes()).orElse(new byte[0]);

        // Signature is serverCert + serverNonce signed with our private key.
        byte[] signature = new byte[serverCertificateBytes.length + serverNonceBytes.length];
        System.arraycopy(serverCertificateBytes, 0, signature, 0, serverCertificateBytes.length);
        System.arraycopy(serverNonceBytes, 0, signature, serverCertificateBytes.length, serverNonceBytes.length);

        SecurityAlgorithm signatureAlgorithm = secureChannel.getSecurityPolicy().getAsymmetricSignatureAlgorithm();

        if (secureChannel.getSecurityPolicy() != SecurityPolicy.None) {
            try {
                PrivateKey privateKey = secureChannel.getKeyPair().getPrivate();

                signature = SignatureUtil.sign(signatureAlgorithm, privateKey, ByteBuffer.wrap(signature));
            } catch (Throwable t) {
                logger.warn("Asymmetric signing failed: {}", t.getMessage(), t);
            }
        }

        return new SignatureData(signatureAlgorithm.getUri(), ByteString.of(signature));
    }

    private class InactivityHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            State currentState = state.get();

            if (currentState instanceof Active) {
                Reactivating reactivating = new Reactivating();

                if (state.compareAndSet(currentState, reactivating)) {
                    OpcUaSession session = ((Active) currentState).session;

                    notifySessionInactive(session);

                    reactivateSession(reactivating, session);
                }
            }

            super.channelInactive(ctx);
        }
    }

    private interface State {
    }

    private class Inactive implements State {
    }

    private class Creating implements State {
        final CompletableFuture<OpcUaSession> sessionFuture;

        Creating() {
            sessionFuture = new CompletableFuture<>();

            sessionFuture.thenAccept(session -> {
                client.getSubscriptionManager().startPublishing();

                notifySessionActive(session);
            });
        }

        Creating(CompletableFuture<OpcUaSession> sessionFuture) {
            this.sessionFuture = sessionFuture;
        }

    }

    private class Activating implements State {
        final CompletableFuture<OpcUaSession> sessionFuture;

        Activating(CompletableFuture<OpcUaSession> sessionFuture) {
            this.sessionFuture = sessionFuture;
        }
    }

    private class Transferring implements State {
        final CompletableFuture<OpcUaSession> sessionFuture;

        Transferring(CompletableFuture<OpcUaSession> sessionFuture) {
            this.sessionFuture = sessionFuture;
        }
    }

    private class Active implements State {
        final OpcUaSession session;
        final CompletableFuture<OpcUaSession> sessionFuture;

        Active(OpcUaSession session, CompletableFuture<OpcUaSession> sessionFuture) {
            this.session = session;
            this.sessionFuture = sessionFuture;
        }
    }

    private class Reactivating implements State {
        final CompletableFuture<OpcUaSession> sessionFuture = new CompletableFuture<>();

        Reactivating() {
            sessionFuture.thenAccept(session -> {
                client.getSubscriptionManager().startPublishing();

                notifySessionActive(session);
            });
        }
    }

    private class Closing implements State {
        final CompletableFuture<OpcUaSession> closeFuture = new CompletableFuture<>();

        Closing() {
            closeFuture.thenAccept(ClientSessionManager.this::notifySessionInactive);
        }
    }

}