Java tutorial
/** * Copyright (c) 2016, 2018 Bosch Software Innovations GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bosch Software Innovations GmbH - initial creation * Red Hat Inc */ package org.eclipse.hono.client.impl; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.client.CredentialsClient; import org.eclipse.hono.client.HonoClient; import org.eclipse.hono.client.MessageConsumer; import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.client.RequestResponseClient; import org.eclipse.hono.client.ServerErrorException; import org.eclipse.hono.client.TenantClient; import org.eclipse.hono.config.ClientConfigProperties; import org.eclipse.hono.connection.ConnectionFactory; import org.eclipse.hono.connection.ConnectionFactoryImpl.ConnectionFactoryBuilder; import org.eclipse.hono.util.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.proton.ProtonClientOptions; import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonDelivery; /** * A helper class for creating Vert.x based clients for Hono's arbitrary APIs. */ public class HonoClientImpl implements HonoClient { private static final Logger LOG = LoggerFactory.getLogger(HonoClientImpl.class); private final Map<String, MessageSender> activeSenders = new HashMap<>(); private final Map<String, RequestResponseClient> activeRequestResponseClients = new HashMap<>(); private final Map<String, Boolean> creationLocks = new HashMap<>(); private final List<Handler<Void>> creationRequests = new ArrayList<>(); private final AtomicBoolean connecting = new AtomicBoolean(false); private final AtomicBoolean shuttingDown = new AtomicBoolean(false); private final ConnectionFactory connectionFactory; private final ClientConfigProperties clientConfigProperties; private final Vertx vertx; private final Object connectionLock = new Object(); private final Context context; private ProtonClientOptions clientOptions; private ProtonConnection connection; private CacheManager cacheManager; private AtomicInteger reconnectAttempts = new AtomicInteger(0); /** * Creates a new client for a set of configuration properties. * <p> * This constructor creates a connection factory using {@link ConnectionFactoryBuilder}. * * @param vertx The Vert.x instance to execute the client on, if {@code null} a new Vert.x instance is used. * @param clientConfigProperties The configuration properties to use. */ public HonoClientImpl(final Vertx vertx, final ClientConfigProperties clientConfigProperties) { if (vertx != null) { this.vertx = vertx; } else { this.vertx = Vertx.vertx(); } this.context = vertx.getOrCreateContext(); this.clientConfigProperties = clientConfigProperties; this.connectionFactory = ConnectionFactoryBuilder.newBuilder(clientConfigProperties).vertx(vertx).build(); } /** * Creates a new client for a set of configuration properties. * <p> * <em>NB</em> Make sure to always use the same set of configuration properties for both * the connection factory as well as the Hono client in order to prevent unexpected behavior. * * @param vertx The Vert.x instance to execute the client on, if {@code null} a new Vert.x instance is used. * @param connectionFactory The factory to use for creating an AMQP connection to the Hono server. * @param clientConfigProperties The configuration properties to use. */ public HonoClientImpl(final Vertx vertx, final ConnectionFactory connectionFactory, final ClientConfigProperties clientConfigProperties) { if (vertx != null) { this.vertx = vertx; } else { this.vertx = Vertx.vertx(); } this.context = vertx.getOrCreateContext(); this.connectionFactory = connectionFactory; this.clientConfigProperties = clientConfigProperties; } /** * Sets a manager for creating cache instances to be used in Hono clients. * * @param manager The cache manager. * @throws NullPointerException if manager is {@code null}. */ public final void setCacheManager(final CacheManager manager) { this.cacheManager = Objects.requireNonNull(manager); } /** * {@inheritDoc} */ @Override public final Future<Boolean> isConnected() { final Future<Boolean> result = Future.future(); context.runOnContext(check -> { result.complete(isConnectedInternal()); }); return result; } /** * Checks if this client is currently connected to the server. * * @return A succeeded future if this client is connected. */ protected final Future<Void> checkConnected() { final Future<Void> result = Future.future(); if (isConnectedInternal()) { result.complete(); } else { result.fail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "not connected")); } return result; } private boolean isConnectedInternal() { return connection != null && !connection.isDisconnected(); } /** * Sets the connection used to interact with the Hono server. * * @param connection The connection to use. */ void setConnection(final ProtonConnection connection) { synchronized (connectionLock) { this.connection = connection; } } /** * Gets the underlying connection object that this client * uses to interact with the server. * * @return The connection. */ protected final ProtonConnection getConnection() { synchronized (connectionLock) { return this.connection; } } /** * {@inheritDoc} */ @Override public final Future<HonoClient> connect() { return connect(null, null); } /** * {@inheritDoc} */ @Override public final Future<HonoClient> connect(final ProtonClientOptions options) { return connect(Objects.requireNonNull(options), null); } /** * {@inheritDoc} */ @Override public final Future<HonoClient> connect(final Handler<ProtonConnection> disconnectHandler) { return connect(null, Objects.requireNonNull(disconnectHandler)); } /** * {@inheritDoc} */ @Override public final Future<HonoClient> connect(final ProtonClientOptions options, final Handler<ProtonConnection> disconnectHandler) { final Future<HonoClient> result = Future.future(); if (shuttingDown.get()) { result.fail(new ClientErrorException(HttpURLConnection.HTTP_CONFLICT, "client is already shut down")); } else { connect(options, result.completer(), disconnectHandler); } return result; } private void connect(final ProtonClientOptions options, final Handler<AsyncResult<HonoClient>> connectionHandler, final Handler<ProtonConnection> disconnectHandler) { context.runOnContext(connect -> { if (isConnectedInternal()) { LOG.debug("already connected to server [{}:{}]", connectionFactory.getHost(), connectionFactory.getPort()); connectionHandler.handle(Future.succeededFuture(this)); } else if (connecting.compareAndSet(false, true)) { if (options == null) { // by default, try to re-connect forever clientOptions = new ProtonClientOptions().setConnectTimeout(200).setReconnectAttempts(-1) .setReconnectInterval(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS); } else { clientOptions = options; } connectionFactory.connect(clientOptions, remoteClose -> onRemoteClose(remoteClose, disconnectHandler), failedConnection -> onRemoteDisconnect(failedConnection, disconnectHandler), conAttempt -> { connecting.compareAndSet(true, false); if (conAttempt.failed()) { if (conAttempt.cause() instanceof SecurityException) { // SASL handshake has failed connectionHandler.handle(Future.failedFuture( new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "failed to authenticate with server"))); } else { reconnect(conAttempt.cause(), connectionHandler, disconnectHandler); } } else { // make sure we try to re-connect as often as we tried to connect initially reconnectAttempts = new AtomicInteger(0); final ProtonConnection newConnection = conAttempt.result(); if (shuttingDown.get()) { // if client was shut down in the meantime, we need to immediately // close again the newly created connection newConnection.closeHandler(null); newConnection.disconnectHandler(null); newConnection.close(); connectionHandler.handle(Future.failedFuture(new ClientErrorException( HttpURLConnection.HTTP_CONFLICT, "client is already shut down"))); } else { setConnection(newConnection); connectionHandler.handle(Future.succeededFuture(this)); } } }); } else { LOG.debug("already trying to connect to server ..."); connectionHandler.handle(Future.failedFuture( new ClientErrorException(HttpURLConnection.HTTP_CONFLICT, "already connecting to server"))); } }); } private void onRemoteClose(final AsyncResult<ProtonConnection> remoteClose, final Handler<ProtonConnection> connectionLossHandler) { if (remoteClose.failed()) { LOG.info("remote server [{}:{}] closed connection with error condition: {}", connectionFactory.getHost(), connectionFactory.getPort(), remoteClose.cause().getMessage()); } else { LOG.info("remote server [{}:{}] closed connection", connectionFactory.getHost(), connectionFactory.getPort()); } connection.disconnectHandler(null); connection.close(); handleConnectionLoss(connectionLossHandler); } private void onRemoteDisconnect(final ProtonConnection con, final Handler<ProtonConnection> connectionLossHandler) { if (con != connection) { LOG.warn("cannot handle failure of unknown connection"); } else { LOG.debug("lost connection to server [{}:{}]", connectionFactory.getHost(), connectionFactory.getPort()); handleConnectionLoss(connectionLossHandler); } } private void handleConnectionLoss(final Handler<ProtonConnection> connectionLossHandler) { if (isConnectedInternal()) { connection.disconnect(); } final ProtonConnection failedConnection = this.connection; setConnection(null); activeSenders.clear(); activeRequestResponseClients.clear(); failAllCreationRequests(); if (connectionLossHandler != null) { connectionLossHandler.handle(failedConnection); } else { reconnect(attempt -> { }, null); } } private void failAllCreationRequests() { for (Iterator<Handler<Void>> iter = creationRequests.iterator(); iter.hasNext();) { iter.next().handle(null); iter.remove(); } } private void reconnect(final Handler<AsyncResult<HonoClient>> connectionHandler, final Handler<ProtonConnection> disconnectHandler) { reconnect(null, connectionHandler, disconnectHandler); } private void reconnect(final Throwable connectionFailureCause, final Handler<AsyncResult<HonoClient>> connectionHandler, final Handler<ProtonConnection> disconnectHandler) { if (shuttingDown.get()) { // no need to try to re-connect connectionHandler.handle(Future.failedFuture(new IllegalStateException("client is shut down"))); } else if (clientOptions.getReconnectAttempts() - reconnectAttempts.get() == 0) { reconnectAttempts = new AtomicInteger(0); LOG.debug("max number of attempts [{}] to re-connect to peer [{}:{}] have been made, giving up", clientOptions.getReconnectAttempts(), connectionFactory.getHost(), connectionFactory.getPort()); if (connectionFailureCause == null) { connectionHandler.handle(Future.failedFuture( new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "failed to connect"))); } else { connectionHandler .handle(Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "failed to connect", connectionFailureCause))); } } else { LOG.trace("scheduling attempt to re-connect ..."); reconnectAttempts.getAndIncrement(); // give Vert.x some time to clean up NetClient vertx.setTimer(clientOptions.getReconnectInterval(), tid -> { LOG.debug("starting attempt [#{}] to re-connect to server [{}:{}]", reconnectAttempts.get(), connectionFactory.getHost(), connectionFactory.getPort()); connect(clientOptions, connectionHandler, disconnectHandler); }); } } /** * {@inheritDoc} */ @Override public final Future<MessageSender> getOrCreateTelemetrySender(final String tenantId) { return getOrCreateTelemetrySender(tenantId, null); } /** * {@inheritDoc} */ @Override public final Future<MessageSender> getOrCreateTelemetrySender(final String tenantId, final String deviceId) { Objects.requireNonNull(tenantId); return getOrCreateSender(TelemetrySenderImpl.getTargetAddress(tenantId, deviceId), () -> createTelemetrySender(tenantId, deviceId)); } private Future<MessageSender> createTelemetrySender(final String tenantId, final String deviceId) { return checkConnected().compose(connected -> { final Future<MessageSender> result = Future.future(); TelemetrySenderImpl.create(context, clientConfigProperties, connection, tenantId, deviceId, onSenderClosed -> { activeSenders.remove(TelemetrySenderImpl.getTargetAddress(tenantId, deviceId)); }, result.completer()); return result; }); } /** * {@inheritDoc} */ @Override public final Future<MessageSender> getOrCreateEventSender(final String tenantId) { return getOrCreateEventSender(tenantId, null); } /** * {@inheritDoc} */ @Override public final Future<MessageSender> getOrCreateEventSender(final String tenantId, final String deviceId) { Objects.requireNonNull(tenantId); return getOrCreateSender(EventSenderImpl.getTargetAddress(tenantId, deviceId), () -> createEventSender(tenantId, deviceId)); } private Future<MessageSender> createEventSender(final String tenantId, final String deviceId) { return checkConnected().compose(connected -> { Future<MessageSender> result = Future.future(); EventSenderImpl.create(context, clientConfigProperties, connection, tenantId, deviceId, onSenderClosed -> { activeSenders.remove(EventSenderImpl.getTargetAddress(tenantId, deviceId)); }, result.completer()); return result; }); } Future<MessageSender> getOrCreateSender(final String key, final Supplier<Future<MessageSender>> newSenderSupplier) { final Future<MessageSender> result = Future.future(); context.runOnContext(get -> { final MessageSender sender = activeSenders.get(key); if (sender != null && sender.isOpen()) { LOG.debug("reusing existing message sender [target: {}, credit: {}]", key, sender.getCredit()); result.complete(sender); } else if (!creationLocks.computeIfAbsent(key, k -> Boolean.FALSE)) { // register a handler to be notified if the underlying connection to the server fails // so that we can fail the result handler passed in final Handler<Void> connectionFailureHandler = connectionLost -> { // remove lock so that next attempt to open a sender doesn't fail creationLocks.remove(key); result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no connection to service")); }; creationRequests.add(connectionFailureHandler); creationLocks.put(key, Boolean.TRUE); LOG.debug("creating new message sender for {}", key); newSenderSupplier.get().setHandler(creationAttempt -> { creationLocks.remove(key); creationRequests.remove(connectionFailureHandler); if (creationAttempt.succeeded()) { MessageSender newSender = creationAttempt.result(); LOG.debug("successfully created new message sender for {}", key); activeSenders.put(key, newSender); result.tryComplete(newSender); } else { LOG.debug("failed to create new message sender for {}", key, creationAttempt.cause()); activeSenders.remove(key); result.tryFail(creationAttempt.cause()); } }); } else { LOG.debug("already trying to create a message sender for {}", key); result.fail( new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no connection to service")); } }); return result; } /** * {@inheritDoc} */ @Override public final Future<MessageConsumer> createTelemetryConsumer(final String tenantId, final Consumer<Message> messageConsumer, final Handler<Void> closeHandler) { return createConsumer(tenantId, () -> newTelemetryConsumer(tenantId, messageConsumer, closeHandler)); } private Future<MessageConsumer> newTelemetryConsumer(final String tenantId, final Consumer<Message> messageConsumer, final Handler<Void> closeHandler) { return checkConnected().compose(con -> { final Future<MessageConsumer> result = Future.future(); TelemetryConsumerImpl.create(context, clientConfigProperties, connection, tenantId, connectionFactory.getPathSeparator(), messageConsumer, result.completer(), closeHook -> closeHandler.handle(null)); return result; }); } /** * {@inheritDoc} */ @Override public final Future<MessageConsumer> createEventConsumer(final String tenantId, final Consumer<Message> eventConsumer, final Handler<Void> closeHandler) { return createEventConsumer(tenantId, (delivery, message) -> eventConsumer.accept(message), closeHandler); } /** * {@inheritDoc} */ @Override public final Future<MessageConsumer> createEventConsumer(final String tenantId, final BiConsumer<ProtonDelivery, Message> messageConsumer, final Handler<Void> closeHandler) { return createConsumer(tenantId, () -> newEventConsumer(tenantId, messageConsumer, closeHandler)); } private Future<MessageConsumer> newEventConsumer(final String tenantId, final BiConsumer<ProtonDelivery, Message> messageConsumer, final Handler<Void> closeHandler) { return checkConnected().compose(con -> { final Future<MessageConsumer> result = Future.future(); EventConsumerImpl.create(context, clientConfigProperties, connection, tenantId, connectionFactory.getPathSeparator(), messageConsumer, result.completer(), closeHook -> closeHandler.handle(null)); return result; }); } Future<MessageConsumer> createConsumer(final String tenantId, final Supplier<Future<MessageConsumer>> newConsumerSupplier) { final Future<MessageConsumer> result = Future.future(); context.runOnContext(get -> { // register a handler to be notified if the underlying connection to the server fails // so that we can fail the result handler passed in final Handler<Void> connectionFailureHandler = connectionLost -> { result.tryFail( new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "connection to server lost")); }; creationRequests.add(connectionFailureHandler); newConsumerSupplier.get().setHandler(attempt -> { creationRequests.remove(connectionFailureHandler); if (attempt.succeeded()) { result.tryComplete(attempt.result()); } else { result.tryFail(attempt.cause()); } }); }); return result; } /** * {@inheritDoc} */ @Override public final Future<CredentialsClient> getOrCreateCredentialsClient(final String tenantId) { Objects.requireNonNull(tenantId); final Future<CredentialsClient> result = Future.future(); getOrCreateRequestResponseClient(CredentialsClientImpl.getTargetAddress(tenantId), () -> newCredentialsClient(tenantId), attempt -> { if (attempt.succeeded()) { result.complete((CredentialsClient) attempt.result()); } else { result.fail(attempt.cause()); } }); return result; } /** * Creates a new instance of {@link CredentialsClient} scoped for the given tenant identifier. * <p> * Custom implementation of {@link CredentialsClient} can be instantiated by overriding this method. Any such * instance should be scoped to the given tenantId. Custom extension of {@link HonoClientImpl} must invoke * {@link #removeCredentialsClient(String)} to cleanup when finished with the client. * * @param tenantId tenant scope for which the client is instantiated * @return a future containing an instance of {@link CredentialsClient} * @see CredentialsClient */ protected Future<RequestResponseClient> newCredentialsClient(final String tenantId) { return checkConnected().compose(connected -> { final Future<CredentialsClient> result = Future.future(); CredentialsClientImpl.create(context, clientConfigProperties, connection, tenantId, this::removeCredentialsClient, this::removeCredentialsClient, result.completer()); return result.map(client -> (RequestResponseClient) client); }); } /** * Removes a credentials client from the list of active clients. * <p> * Once a client has been removed, the next invocation * of the corresponding <em>getOrCreateCredentialsClient</em> * method will result in a new client being created * (and added to the list of active clients). * * @param tenantId The tenant that the client is scoped to. */ protected final void removeCredentialsClient(final String tenantId) { final String targetAddress = CredentialsClientImpl.getTargetAddress(tenantId); removeActiveRequestResponseClient(targetAddress); } private void removeActiveRequestResponseClient(final String targetAddress) { final RequestResponseClient client = activeRequestResponseClients.remove(targetAddress); if (client != null) { client.close(s -> { }); LOG.debug("closed and removed client for [{}]", targetAddress); } } /** * {@inheritDoc} */ @Override public final Future<RegistrationClient> getOrCreateRegistrationClient(final String tenantId) { Objects.requireNonNull(tenantId); final Future<RegistrationClient> result = Future.future(); getOrCreateRequestResponseClient(RegistrationClientImpl.getTargetAddress(tenantId), () -> newRegistrationClient(tenantId), attempt -> { if (attempt.succeeded()) { result.complete((RegistrationClient) attempt.result()); } else { result.fail(attempt.cause()); } }); return result; } /** * Creates a new instance of {@link RegistrationClient} scoped for the given tenantId. * <p> * Custom implementation of {@link RegistrationClient} can be instantiated by overriding this method. Any such * instance should be scoped to the given tenantId. Custom extension of {@link HonoClientImpl} must invoke * {@link #removeRegistrationClient(String)} to cleanup when finished with the client. * * @param tenantId tenant scope for which the client is instantiated * @return a future containing an instance of {@link RegistrationClient} * @see RegistrationClient */ protected Future<RequestResponseClient> newRegistrationClient(final String tenantId) { Objects.requireNonNull(tenantId); return checkConnected().compose(connected -> { final Future<RegistrationClient> result = Future.future(); RegistrationClientImpl.create(context, clientConfigProperties, cacheManager, connection, tenantId, this::removeRegistrationClient, this::removeRegistrationClient, result.completer()); return result.map(client -> (RequestResponseClient) client); }); } /** * Removes a registration client from the list of active clients. * <p> * Once a client has been removed, the next invocation * of the corresponding <em>getOrCreateRegistrationClient</em> * method will result in a new client being created * (and added to the list of active clients). * * @param tenantId The tenant that the client is scoped to. */ protected final void removeRegistrationClient(final String tenantId) { final String targetAddress = RegistrationClientImpl.getTargetAddress(tenantId); removeActiveRequestResponseClient(targetAddress); } /** * {@inheritDoc} */ @Override public Future<TenantClient> getOrCreateTenantClient() { final Future<TenantClient> result = Future.future(); getOrCreateRequestResponseClient(TenantClientImpl.getTargetAddress(), () -> newTenantClient(), attempt -> { if (attempt.succeeded()) { result.complete((TenantClient) attempt.result()); } else { result.fail(attempt.cause()); } }); return result; } private Future<RequestResponseClient> newTenantClient() { return checkConnected().compose(connected -> { final Future<TenantClient> result = Future.future(); TenantClientImpl.create(context, clientConfigProperties, cacheManager, connection, this::removeTenantClient, this::removeTenantClient, result.completer()); return result.map(client -> (RequestResponseClient) client); }); } /** * * @param tenantId The tenantId (not used for this client). */ private void removeTenantClient(final String tenantId) { final String targetAddress = TenantClientImpl.getTargetAddress(); removeActiveRequestResponseClient(targetAddress); } /** * Gets an existing or creates a new request-response client for a particular service. * * @param key The key to look-up the client by. * @param clientSupplier A consumer for an attempt to create a new client. * @param resultHandler The handler to inform about the outcome of the operation. */ void getOrCreateRequestResponseClient(final String key, final Supplier<Future<RequestResponseClient>> clientSupplier, final Handler<AsyncResult<RequestResponseClient>> resultHandler) { context.runOnContext(get -> { final RequestResponseClient client = activeRequestResponseClients.get(key); if (client != null && client.isOpen()) { LOG.debug("reusing existing client [target: {}]", key); resultHandler.handle(Future.succeededFuture(client)); } else if (!creationLocks.computeIfAbsent(key, k -> Boolean.FALSE)) { // register a handler to be notified if the underlying connection to the server fails // so that we can fail the result handler passed in final Handler<Void> connectionFailureHandler = connectionLost -> { // remove lock so that next attempt to open a sender doesn't fail creationLocks.remove(key); resultHandler .handle(Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no connection to service"))); }; creationRequests.add(connectionFailureHandler); creationLocks.put(key, Boolean.TRUE); LOG.debug("creating new client for {}", key); clientSupplier.get().setHandler(creationAttempt -> { if (creationAttempt.succeeded()) { LOG.debug("successfully created new client for {}", key); activeRequestResponseClients.put(key, creationAttempt.result()); } else { LOG.debug("failed to create new client for {}", key, creationAttempt.cause()); activeRequestResponseClients.remove(key); } creationLocks.remove(key); creationRequests.remove(connectionFailureHandler); resultHandler.handle(creationAttempt); }); } else { LOG.debug("already trying to create a client for {}", key); resultHandler.handle(Future.failedFuture( new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no connection to service"))); } }); } /** * {@inheritDoc} */ @Override public final void shutdown() { final CountDownLatch latch = new CountDownLatch(1); shutdown(done -> { if (done.succeeded()) { latch.countDown(); } else { LOG.error("could not close connection to server", done.cause()); } }); try { if (!latch.await(5, TimeUnit.SECONDS)) { LOG.error("shutdown of client timed out after 5 seconds"); } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } } /** * {@inheritDoc} */ @Override public final void shutdown(final Handler<AsyncResult<Void>> completionHandler) { if (shuttingDown.compareAndSet(Boolean.FALSE, Boolean.TRUE)) { context.runOnContext(shutDown -> { if (isConnectedInternal()) { shutdownConnection(completionHandler); } else { LOG.info("connection to server [{}:{}] already closed", connectionFactory.getHost(), connectionFactory.getPort()); completionHandler.handle(Future.succeededFuture()); } }); } else { completionHandler.handle(Future.failedFuture(new IllegalStateException("already shutting down"))); } } private void shutdownConnection(final Handler<AsyncResult<Void>> completionHandler) { LOG.info("closing connection to server [{}:{}]...", connectionFactory.getHost(), connectionFactory.getPort()); connection.disconnectHandler(null); // make sure we are not trying to re-connect connection.closeHandler(closedCon -> { if (closedCon.succeeded()) { LOG.info("closed connection to server [{}:{}]", connectionFactory.getHost(), connectionFactory.getPort()); } else { LOG.info("closed connection to server [{}:{}]", connectionFactory.getHost(), connectionFactory.getPort(), closedCon.cause()); } connection.disconnect(); if (completionHandler != null) { completionHandler.handle(Future.succeededFuture()); } }).close(); } }