com.microsoft.azure.sdk.iot.service.messaging.MessagingClient Maven / Gradle / Ivy
Show all versions of iot-service-client Show documentation
/*
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project root for full license information.
*/
package com.microsoft.azure.sdk.iot.service.messaging;
import com.azure.core.credential.AzureSasCredential;
import com.azure.core.credential.TokenCredential;
import com.microsoft.azure.sdk.iot.service.auth.IotHubConnectionStringBuilder;
import com.microsoft.azure.sdk.iot.service.exceptions.IotHubException;
import com.microsoft.azure.sdk.iot.service.exceptions.IotHubUnauthorizedException;
import com.microsoft.azure.sdk.iot.service.transport.TransportUtils;
import com.microsoft.azure.sdk.iot.service.transport.amqps.CloudToDeviceMessageConnectionHandler;
import com.microsoft.azure.sdk.iot.service.transport.amqps.ReactorRunner;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* A client for sending cloud to device and cloud to module messages. For more details on what cloud to device messages
* are, see this document.
*
*
* This client relies on a persistent amqp/amqp_ws connection to IoT Hub that may break due to network instability.
* While optional to monitor, users are highly encouraged to utilize the errorProcessorHandler defined in the
* {@link MessagingClientOptions} when constructing this client in order to monitor the connection state and to re-open
* the connection when needed. See the messaging client sample in this repo for best practices for monitoring and handling
* disconnection events.
*
*/
@Slf4j
public final class MessagingClient
{
private static final int START_REACTOR_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 seconds
private static final int STOP_REACTOR_TIMEOUT_MILLISECONDS = 5 * 1000; // 5 seconds
private static final int MESSAGE_SEND_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 seconds
private final Consumer errorProcessor; // may be null if user doesn't provide one
private final CloudToDeviceMessageConnectionHandler cloudToDeviceMessageConnectionHandler;
private ReactorRunner reactorRunner;
private final String hostName;
/**
* Construct a MessagingClient from the specified connection string
* @param connectionString The connection string for the IotHub
* @param protocol The protocol that the client will communicate to IoT Hub over.
*/
public MessagingClient(String connectionString, IotHubServiceClientProtocol protocol)
{
this(connectionString, protocol, MessagingClientOptions.builder().build());
}
/**
* Construct a MessagingClient from the specified connection string
* @param connectionString The connection string for the IotHub
* @param protocol The protocol that the client will communicate to IoT Hub over.
* @param options The connection options to use when connecting to the service. May not be null.
*/
public MessagingClient(
String connectionString,
IotHubServiceClientProtocol protocol,
MessagingClientOptions options)
{
if (Tools.isNullOrEmpty(connectionString))
{
throw new IllegalArgumentException(connectionString);
}
if (options == null)
{
throw new IllegalArgumentException("MessagingClientOptions cannot be null for this constructor");
}
this.errorProcessor = options.getErrorProcessor();
this.hostName = IotHubConnectionStringBuilder.createIotHubConnectionString(connectionString).getHostName();
this.cloudToDeviceMessageConnectionHandler =
new CloudToDeviceMessageConnectionHandler(
connectionString,
protocol,
this.errorProcessor,
options.getProxyOptions(),
options.getSslContext(),
options.getKeepAliveInterval());
commonConstructorSetup();
}
/**
* Create a {@link MessagingClient} instance with a custom {@link TokenCredential} to allow for finer grain control
* of authentication tokens used in the underlying connection.
*
* @param hostName The hostname of your IoT Hub instance (For instance, "your-iot-hub.azure-devices.net")
* @param credential The custom {@link TokenCredential} that will provide authentication tokens to
* this library when they are needed. The provided tokens must be Json Web Tokens.
* @param protocol The protocol that the client will communicate to IoT Hub over.
*/
public MessagingClient(
String hostName,
TokenCredential credential,
IotHubServiceClientProtocol protocol)
{
this(hostName, credential, protocol, MessagingClientOptions.builder().build());
}
/**
* Create a {@link MessagingClient} instance with a custom {@link TokenCredential} to allow for finer grain control
* of authentication tokens used in the underlying connection.
*
* @param hostName The hostname of your IoT Hub instance (For instance, "your-iot-hub.azure-devices.net")
* @param credential The custom {@link TokenCredential} that will provide authentication tokens to
* this library when they are needed. The provided tokens must be Json Web Tokens.
* @param protocol The protocol that the client will communicate to IoT Hub over.
* @param options The connection options to use when connecting to the service. May not be null.
*/
public MessagingClient(
String hostName,
TokenCredential credential,
IotHubServiceClientProtocol protocol,
MessagingClientOptions options)
{
Objects.requireNonNull(credential);
if (Tools.isNullOrEmpty(hostName))
{
throw new IllegalArgumentException("HostName cannot be null or empty");
}
if (options == null)
{
throw new IllegalArgumentException("MessagingClientOptions cannot be null for this constructor");
}
if (options.getProxyOptions() != null && protocol != IotHubServiceClientProtocol.AMQPS_WS)
{
throw new UnsupportedOperationException("Proxies are only supported over AMQPS_WS");
}
this.errorProcessor = options.getErrorProcessor();
this.hostName = hostName;
this.cloudToDeviceMessageConnectionHandler =
new CloudToDeviceMessageConnectionHandler(
hostName,
credential,
protocol,
this.errorProcessor,
options.getProxyOptions(),
options.getSslContext(),
options.getKeepAliveInterval());
commonConstructorSetup();
}
/**
* Create a {@link MessagingClient} instance with an instance of {@link AzureSasCredential}.
*
* @param hostName The hostname of your IoT Hub instance (For instance, "your-iot-hub.azure-devices.net")
* @param azureSasCredential The SAS token provider that will be used for authentication.
* @param protocol The protocol that the client will communicate to IoT Hub over.
*/
public MessagingClient(
String hostName,
AzureSasCredential azureSasCredential,
IotHubServiceClientProtocol protocol)
{
this(hostName, azureSasCredential, protocol, MessagingClientOptions.builder().build());
}
/**
* Create a {@link MessagingClient} instance with an instance of {@link AzureSasCredential}.
*
* @param hostName The hostname of your IoT Hub instance (For instance, "your-iot-hub.azure-devices.net")
* @param azureSasCredential The SAS token provider that will be used for authentication.
* @param protocol The protocol that the client will communicate to IoT Hub over.
* @param options The connection options to use when connecting to the service. May not be null.
*/
public MessagingClient(
String hostName,
AzureSasCredential azureSasCredential,
IotHubServiceClientProtocol protocol,
MessagingClientOptions options)
{
Objects.requireNonNull(azureSasCredential);
Objects.requireNonNull(options);
if (options.getProxyOptions() != null && protocol != IotHubServiceClientProtocol.AMQPS_WS)
{
throw new UnsupportedOperationException("Proxies are only supported over AMQPS_WS");
}
this.errorProcessor = options.getErrorProcessor();
this.hostName = hostName;
this.cloudToDeviceMessageConnectionHandler =
new CloudToDeviceMessageConnectionHandler(
hostName,
azureSasCredential,
protocol,
this.errorProcessor,
options.getProxyOptions(),
options.getSslContext(),
options.getKeepAliveInterval());
commonConstructorSetup();
}
private static void commonConstructorSetup()
{
log.debug("Initialized a MessagingClient instance using SDK version {}", TransportUtils.serviceVersion);
}
/**
* Open this client so that it can begin sending cloud to device and/or cloud to module messages. Once opened, you should
* call {@link #close()} once no more messages will be sent in order to free up network resources. If this
* client is already open, then this function will do nothing.
*
* @throws IotHubException If any IoT Hub level exceptions occur such as an {@link IotHubUnauthorizedException}.
* @throws IOException If any network level exceptions occur such as the connection timing out.
* @throws InterruptedException If this thread is interrupted while waiting for the connection to the service to open.
* @throws TimeoutException If the connection is not established before the default timeout.
*/
public synchronized void open() throws IotHubException, IOException, InterruptedException, TimeoutException
{
open(START_REACTOR_TIMEOUT_MILLISECONDS);
}
/**
* Open this client so that it can begin sending cloud to device and/or cloud to module messages. Once opened, this
* client should call {@link #close()} once no more messages will be sent in order to free up network resources. If this
* client is already open, then this function will do nothing.
*
* @param timeoutMilliseconds the maximum number of milliseconds to wait for the underlying amqp connection to open.
* If this value is 0, it will have an infinite timeout.
* @throws IotHubException If any IoT Hub level exceptions occur such as an {@link IotHubUnauthorizedException}.
* @throws IOException If any network level exceptions occur such as the connection timing out.
* @throws InterruptedException If this thread is interrupted while waiting for the connection to the service to open.
* @throws TimeoutException If the connection is not established before the provided timeout.
*/
public synchronized void open(int timeoutMilliseconds) throws IotHubException, IOException, InterruptedException, TimeoutException
{
if (this.isOpen())
{
//already open
return;
}
if (timeoutMilliseconds < 0)
{
throw new IllegalArgumentException("timeoutMilliseconds must be greater than or equal to 0");
}
AtomicReference iotHubException = new AtomicReference<>(null);
AtomicReference ioException = new AtomicReference<>(null);
log.debug("Opening MessagingClient");
this.reactorRunner = new ReactorRunner(
this.hostName,
"MessagingClient",
this.cloudToDeviceMessageConnectionHandler);
final CountDownLatch openLatch = new CountDownLatch(1);
this.cloudToDeviceMessageConnectionHandler.setOnConnectionOpenedCallback(openLatch::countDown);
new Thread(() ->
{
try
{
reactorRunner.run();
log.trace("MessagingClient Amqp reactor stopped, checking that the connection was opened");
this.cloudToDeviceMessageConnectionHandler.verifyConnectionWasOpened();
log.trace("MessagingClient reactor did successfully open the connection, returning without exception");
}
catch (IOException e)
{
ioException.set(e);
}
catch (IotHubException e)
{
iotHubException.set(e);
}
finally
{
openLatch.countDown();
}
}).start();
boolean timedOut = !openLatch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS);
if (timedOut)
{
throw new TimeoutException("Timed out waiting for the connection to the service to open");
}
// if an IOException or IotHubException was encountered in the reactor thread, throw it here
if (ioException.get() != null)
{
throw ioException.get();
}
if (iotHubException.get() != null)
{
throw iotHubException.get();
}
log.info("Opened MessagingClient");
}
/**
* Close this client and release all network resources tied to it. Once closed, this client can be re-opened by
* calling {@link #open()}. If this client is already closed, this function will do nothing.
*
* @throws InterruptedException if this function is interrupted while waiting for the connection to close down all
* network resources.
*/
public synchronized void close() throws InterruptedException
{
this.close(STOP_REACTOR_TIMEOUT_MILLISECONDS);
}
/**
* Close this client and release all network resources tied to it. Once closed, this client can be re-opened by
* calling {@link #open()}. If this client is already closed, this function will do nothing.
*
* @param timeoutMilliseconds the maximum number of milliseconds to wait for the underlying amqp connection to close.
* If this value is 0, it will have an infinite timeout. If the provided timeout has passed and the connection has
* not closed gracefully, then the connection will be forcefully closed and no exception will be thrown.
* @throws InterruptedException if this function is interrupted while waiting for the connection to close down all
* network resources.
*/
public synchronized void close(int timeoutMilliseconds) throws InterruptedException
{
if (this.reactorRunner == null)
{
return;
}
if (timeoutMilliseconds < 0)
{
throw new IllegalArgumentException("timeoutMilliseconds must be greater than or equal to 0");
}
this.reactorRunner.stop(timeoutMilliseconds);
this.reactorRunner = null;
log.info("Closed MessagingClient");
}
/**
* Send a cloud to device message to the device with the provided device id.
*
*
* This method is a blocking call that will wait for the sent message to be acknowledged by the service before returning.
* This is provided for simplicity and for applications that aren't concerned with throughput. For applications that
* need to provided higher throughput of sent cloud to device messages, users should use {@link #sendAsync(String, Message, Consumer, Object)}
* as demonstrated in the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device to send the cloud to device message to.
* @param message the message to send to the device.
* @throws IotHubException If any IoT Hub level exception is thrown. For instance, if the provided message exceeds
* the IoT Hub message size limit, {@link com.microsoft.azure.sdk.iot.service.exceptions.IotHubMessageTooLargeException} will be thrown.
* @throws InterruptedException If this function is interrupted while waiting for the cloud to device message to be acknowledged
* by the service.
* @throws TimeoutException If the sent message isn't acknowledged by the service within the default timeout.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void send(String deviceId, Message message) throws IotHubException, InterruptedException, TimeoutException, IllegalStateException
{
this.send(deviceId, null, message, MESSAGE_SEND_TIMEOUT_MILLISECONDS);
}
/**
* Send a cloud to device message to the device with the provided device id.
*
*
* This method is a blocking call that will wait for the sent message to be acknowledged by the service before returning.
* This is provided for simplicity and for applications that aren't concerned with throughput. For applications that
* need to provided higher throughput of sent cloud to device messages, users should use {@link #sendAsync(String, Message, Consumer, Object)}
* as demonstrated in the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device to send the cloud to device message to.
* @param message the message to send to the device.
* @param timeoutMilliseconds the maximum number of milliseconds to wait for the message to be sent before timing out and throwing an {@link IotHubException}.
* @throws IotHubException If any IoT Hub level exception is thrown. For instance, if the provided message exceeds
* the IoT Hub message size limit, {@link com.microsoft.azure.sdk.iot.service.exceptions.IotHubMessageTooLargeException} will be thrown.
* @throws InterruptedException If this function is interrupted while waiting for the cloud to device message to be acknowledged
* by the service.
* @throws TimeoutException If the sent message isn't acknowledged by the service within the provided timeout.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void send(String deviceId, Message message, int timeoutMilliseconds) throws IotHubException, InterruptedException, TimeoutException, IllegalStateException
{
this.send(deviceId, null, message, timeoutMilliseconds);
}
/**
* Send a cloud to device message to the module with the provided module id on the device with the provided device Id.
*
*
* This method is a blocking call that will wait for the sent message to be acknowledged by the service before returning.
* This is provided for simplicity and for applications that aren't concerned with throughput. For applications that
* need to provided higher throughput of sent cloud to device messages, users should use {@link #sendAsync(String, String, Message, Consumer, Object)}
* as demonstrated in the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device that contains the module that the message is being sent to.
* @param moduleId the Id of the module to send the cloud to device message to.
* @param message the message to send to the device.
* @throws IotHubException If any IoT Hub level exception is thrown. For instance, if the provided message exceeds
* the IoT Hub message size limit, {@link com.microsoft.azure.sdk.iot.service.exceptions.IotHubMessageTooLargeException} will be thrown.
* @throws InterruptedException If this function is interrupted while waiting for the cloud to device message to be acknowledged
* by the service.
* @throws TimeoutException If the sent message isn't acknowledged by the service within the default timeout.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void send(String deviceId, String moduleId, Message message) throws IotHubException, InterruptedException, TimeoutException, IllegalStateException
{
this.send(deviceId, moduleId, message, MESSAGE_SEND_TIMEOUT_MILLISECONDS);
}
/**
* Send a cloud to device message to the module with the provided module id on the device with the provided device Id.
*
*
* This method is a blocking call that will wait for the sent message to be acknowledged by the service before returning.
* This is provided for simplicity and for applications that aren't concerned with throughput. For applications that
* need to provided higher throughput of sent cloud to device messages, users should use {@link #sendAsync(String, String, Message, Consumer, Object)}
* as demonstrated in the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device that contains the module that the message is being sent to.
* @param moduleId the Id of the module to send the cloud to device message to.
* @param message the message to send to the device.
* @param timeoutMilliseconds the maximum number of milliseconds to wait for the message to be sent before timing out and throwing an {@link IotHubException}.
* @throws IotHubException If any IoT Hub level exception is thrown. For instance, if the provided message exceeds
* the IoT Hub message size limit, {@link com.microsoft.azure.sdk.iot.service.exceptions.IotHubMessageTooLargeException} will be thrown.
* @throws InterruptedException If this function is interrupted while waiting for the cloud to device message to be acknowledged
* by the service.
* @throws TimeoutException If the sent message isn't acknowledged by the service within the provided timeout.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void send(String deviceId, String moduleId, Message message, int timeoutMilliseconds) throws IotHubException, InterruptedException, TimeoutException, IllegalStateException
{
if (timeoutMilliseconds < 0)
{
throw new IllegalArgumentException("timeoutMilliseconds must be greater than or equal to 0");
}
AtomicReference exception = new AtomicReference<>();
final CountDownLatch messageSentLatch = new CountDownLatch(1);
Consumer onMessageAcknowledgedCallback = sendResult ->
{
if (sendResult.wasSentSuccessfully())
{
log.trace("Message acknowledged callback executed for cloud to device message with correlation id {} that was successfully sent.", sendResult.getCorrelationId());
}
else
{
log.trace("Message acknowledged callback executed for cloud to device message with correlation id {} that failed to send.", sendResult.getCorrelationId());
exception.set(sendResult.getException());
}
messageSentLatch.countDown();
};
this.sendAsync(deviceId, moduleId, message, onMessageAcknowledgedCallback, null);
if (timeoutMilliseconds == 0)
{
// wait indefinitely
messageSentLatch.await();
}
else
{
boolean timedOut = !messageSentLatch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS);
if (timedOut)
{
throw new TimeoutException("Timed out waiting for message to be acknowledged");
}
}
if (exception.get() != null)
{
throw exception.get();
}
}
/**
* Asynchronously send a cloud to device message to the device with the provided device Id.
*
* Unlike the synchronous version of this function, this function does not throw any exceptions. Instead, any exception
* encountered while sending the message will be provided in the {@link SendResult} provided in the onMessageSentCallback.
* To see an example of how this looks, see the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device to send the cloud to device message to.
* @param message the message to send to the device.
* @param onMessageSentCallback the callback that will be executed when the message has either successfully been
* sent, or has failed to send. May be null if you don't care if the sent message is acknowledged by the service.
* @param context user defined context that will be provided in the onMessageSentCallback callback when it executes. May be null.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void sendAsync(String deviceId, Message message, Consumer onMessageSentCallback, Object context) throws IllegalStateException
{
this.sendAsync(deviceId, null, message, onMessageSentCallback, context);
}
/**
* Asynchronously send a cloud to device message to the module with the provided module id on the device with the provided device Id.
*
* Unlike the synchronous version of this function, this function does not throw any exceptions. Instead, any exception
* encountered while sending the message will be provided in the {@link SendResult} provided in the onMessageSentCallback.
* To see an example of how this looks, see the messaging client performance sample in this repo.
*
* @param deviceId the Id of the device that contains the module that the message is being sent to.
* @param moduleId the Id of the module to send the cloud to device message to.
* @param message the message to send to the device.
* @param onMessageSentCallback the callback that will be executed when the message has either successfully been
* sent, or has failed to send. May be null if you don't care if the sent message is acknowledged by the service.
* @param context user defined context that will be provided in the onMessageSentCallback callback when it executes. May be null.
* @throws IllegalStateException if the client has not been opened yet, or is closed for any other reason such as connectivity loss.
*/
public void sendAsync(String deviceId, String moduleId, Message message, Consumer onMessageSentCallback, Object context) throws IllegalStateException
{
if (!this.isOpen())
{
throw new IllegalStateException("Client must be opened before any message can be sent");
}
if (moduleId == null)
{
log.info("Sending cloud to device message with correlation id {}", message.getCorrelationId());
}
else
{
log.info("Sending cloud to module message with correlation id {}", message.getCorrelationId());
}
this.cloudToDeviceMessageConnectionHandler.sendAsync(deviceId, moduleId, message, onMessageSentCallback, context);
}
/**
* Returns true if this client is currently open and false otherwise. This client may lose connectivity due to network issues,
* so this value may be false even if you have not closed the client yourself. Monitoring the optional errorProcessor
* that can be set in {@link MessagingClientOptions} will provide the context on when connection loss events occur, and
* why they occurred.
*
* @return true if this client is currently open and false otherwise.
*/
public boolean isOpen()
{
return this.reactorRunner != null && this.reactorRunner.isRunning();
}
}