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

com.microsoft.azure.iothub.transport.amqps.AmqpsIotHubConnection Maven / Gradle / Ivy

There is a newer version: 1.0.16
Show newest version
/*
 * 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.iothub.transport.amqps;

import com.microsoft.azure.iothub.DeviceClientConfig;
import com.microsoft.azure.iothub.IotHubClientProtocol;
import com.microsoft.azure.iothub.IotHubMessageResult;
import com.microsoft.azure.iothub.auth.IotHubSasToken;
import com.microsoft.azure.iothub.net.IotHubUri;
import com.microsoft.azure.iothub.transport.TransportUtils;
import org.apache.qpid.proton.*;
import org.apache.qpid.proton.engine.BaseHandler;
import org.apache.qpid.proton.engine.Event;
import org.apache.qpid.proton.message.Message;
import org.apache.qpid.proton.reactor.Reactor;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.TimeoutException;

/**
 * An AMQPS IotHub Session for a device and an IotHub. This class contains functionality for
 */
public final class AmqpsIotHubConnection extends BaseHandler {
    private static final int DEFAULT_DELIVERY_WAIT_TIME_SECONDS = 5;

    private IotHubReactor iotHubReactor;

    /** Future that gets completed once the reactor is ready to begin sending and receiving */
    protected CompletableFuture reactorReady;

    /** The state of the Reactor. */
    public enum ReactorState
    {
        OPEN, CLOSED
    } ReactorState state;

    /** Semaphore to restrict multiple calls to asynchronously run the Reactor. */
    private Semaphore reactorSemaphore = new Semaphore(1);

    //==============================================================================
    //Connection Variables
    //==============================================================================
    /** The address string of the IoT Hub. */
    private String hostName;
    /** The username string to use SASL authentication. */
    private String userName;
    /** The ID for the associated device. */
    private String deviceID;
    /** Indicates if use AMQP over WEBSOCKET or AMQP */
    private final IotHubClientProtocol iotHubClientProtocol;

    /** The {@link DeviceClientConfig} for the associated device. */
    protected DeviceClientConfig config;

    //==============================================================================
    //Sender Variables
    //==============================================================================
    /** Map of Proton-J {@link org.apache.qpid.proton.engine.Delivery} object hash codes to the corresponding message {@link Tuple}. */
    private Map, byte[], Object>> inProgressMessageMap;
    /** Integer representing the maximum allowable size of the {@link AmqpsIotHubConnection#inProgressMessageMap}. This integer equals the AMQP link credit received when the Sender link is attached. */
	private int maxQueueSize;

    //==============================================================================
    //Receiver Variables
    //==============================================================================
    /** Queue of messaged received. */
    private Queue receivedMessageQueue;

    //==============================================================================
    //Class Variables
    //==============================================================================
    /** The underlying {@link AmqpsIotHubConnectionBaseHandler} implementing lower level callbacks.*/
    private AmqpsIotHubConnectionBaseHandler amqpsHandler;
    /** The completion status for this {@link AmqpsIotHubConnection}. */
    private CompletableFuture completionStatus;
    /** The last received {@link AmqpsMessage} from the {@link AmqpsIotHubConnectionBaseHandler}. */
    private AmqpsMessage lastMessage = null;

    /**
     * Constructor to set up connection parameters using the {@link DeviceClientConfig}.
     *
     * @param config The {@link DeviceClientConfig} corresponding to the device associated with this {@link com.microsoft.azure.iothub.DeviceClient}.
     */
    public AmqpsIotHubConnection(DeviceClientConfig config, IotHubClientProtocol iotHubClientProtocol){
        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_002: [The constructor shall throw a new IllegalArgumentException if any of the parameters of the configuration is null or empty.]
        if(config == null){
            throw new IllegalArgumentException("The DeviceClientConfig cannot be null.");
        }
        if(config.getIotHubHostname() == null || config.getIotHubHostname().length() == 0)
        {
            throw new IllegalArgumentException("hostName cannot be null or empty.");
        }
        if (config.getDeviceId() == null || config.getDeviceId().length() == 0)
        {
            throw new IllegalArgumentException("deviceID cannot be null or empty.");
        }
        if(config.getIotHubName() == null || config.getIotHubName().length() == 0)
        {
            throw new IllegalArgumentException("hubName cannot be null or empty.");
        }
        if(config.getDeviceKey() == null || config.getDeviceKey().length() == 0)
        {
            throw new IllegalArgumentException("deviceKey cannot be null or empty.");
        }

        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_001: [The constructor shall save the configuration.]
        this.config = config;

        String iotHubHostname = this.config.getIotHubHostname();
        String iotHubName = this.config.getIotHubName();
        String deviceId = this.config.getDeviceId();
        String iotHubUser = deviceId + "@sas." + iotHubName;

        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_006: [The constructor shall initialize a new private map for messages that are in progress.]
        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_007: [The constructor shall initialize new private Futures for the status of the Connection and Reactor.]
        this.hostName = iotHubHostname;
        this.userName = iotHubUser;
        this.deviceID = deviceId;
        this.iotHubClientProtocol = iotHubClientProtocol;

        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_004: [The constructor shall set it’s state to CLOSED.]
        this.state = ReactorState.CLOSED;

        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_005: [The constructor shall initialize a new private queue for received messages.]
        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_006: [The constructor shall initialize a new private map for messages that are in progress.]
        receivedMessageQueue = new LinkedBlockingQueue<>();
        inProgressMessageMap = new HashMap<>();

        this.maxQueueSize = -1;

        // Codes_SRS_AMQPSIOTHUBCONNECTION_14_007: [The constructor shall initialize new private Futures for the status of the Connection and Reactor.]
        completionStatus = new CompletableFuture<>();
        reactorReady = new CompletableFuture<>();
    }

    /**
     * Opens the {@link AmqpsIotHubConnection} creating a new {@link AmqpsIotHubConnectionBaseHandler}.
     * 

* If the current {@link AmqpsIotHubConnection.ReactorState} is not OPEN, this method * will create a new {@link IotHubSasToken} and use it to create a new {@link AmqpsIotHubConnectionBaseHandler}. This method will * start the {@link Reactor}, set the current {@link AmqpsIotHubConnection.ReactorState} * to OPEN, and open the {@link AmqpsIotHubConnection} for sending. *

* * @throws IOException if the {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. * @throws InterruptedException if there is a problem acquiring the semaphore for the {@link Reactor}. * @throws ExecutionException If the {@link CompletableFuture} in {@link AmqpsIotHubConnection#reactorReady()} completed exceptionally */ public void open() throws IOException, InterruptedException, ExecutionException { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_011: [If the AMQPS connection is already open, the function shall do nothing.] if(this.state != ReactorState.OPEN) { IotHubSasToken sasToken = new IotHubSasToken( IotHubUri.getResourceUri(this.config.getIotHubHostname(), this.config.getDeviceId()), this.config.getDeviceKey(), System.currentTimeMillis() / 1000l + this.config.getTokenValidSecs() + 1l); // Codes_SRS_AMQPSIOTHUBCONNECTION_14_008: [The function shall initialize its AmqpsIotHubConnectionBaseHandler // using the saved host name, user name, device ID and sas token.] this.amqpsHandler = new AmqpsIotHubConnectionBaseHandler(this.hostName, this.userName, sasToken.toString(), this.deviceID, this.iotHubClientProtocol, this); // Codes_SRS_AMQPSIOTHUBCONNECTION_14_009: [The function shall open the Amqps connection and trigger the Reactor (Proton) to begin running.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_012: [If the AmqpsIotHubConnectionBaseHandler becomes invalidated before the Reactor (Proton) starts, the function shall throw an IOException.] this.startReactorAsync(); // Codes_SRS_AMQPSIOTHUBCONNECTION_14_010: [Once the Reactor (Proton) is ready, the function shall set its state to OPEN.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_031: [ The function shall get the link credit from it's AmqpsIotHubConnectionBaseHandler and set the private maxQueueSize member variable. ] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_032: [ The function shall successfully complete it’s CompletableFuture status member variable. ] try { this.reactorReady(); //This is a blocking call, will return once the link credit is available from the AmqpsIotHubBaseHandler this.maxQueueSize = this.amqpsHandler.getLinkCredit(); }catch(TimeoutException e){ this.amqpsHandler.shutdown(); throw new ExecutionException("The request to get the link credit from the AmqpsIotHubBaseHandler timed out.", e); }catch(Exception e){ this.amqpsHandler.shutdown(); throw e; } this.completionStatus.complete(new Boolean(true)); this.state = ReactorState.OPEN; } //TODO: Should this wrap all exceptions in an IOException and only throw that? } /** * Closes the {@link AmqpsIotHubConnection} and invalidates the {@link Reactor} object. *

* If the current {@link AmqpsIotHubConnection.ReactorState} is not CLOSED, this function * will invalidate the current {@link Reactor} object, set the current {@link AmqpsIotHubConnection.ReactorState} * to CLOSED, and free the current {@link AmqpsIotHubConnectionBaseHandler}. *

*/ public synchronized void close(){ // Codes_SRS_AMQPSIOTHUBCONNECTION_14_013: [The function shall invalidate the private Reactor (Proton) member variable.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_014: [The function shall free the AmqpsIotHubConnectionBaseHandler.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_015: [The function shall close the AMQPS connection.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_016: [If the AMQPS connection is already closed, the function shall do nothing.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_033: [The function shall close the AmqpsIotHubConnectionBaseHandler.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_034: [The function shall exceptionally complete all remaining messages that are currently in progress and clear the queue.] if(this.state != ReactorState.CLOSED) { this.amqpsHandler.shutdown(); this.clearInProgressMap(); this.freeReactor(); this.freeHandler(); this.state = ReactorState.CLOSED; this.maxQueueSize = -1; } } /** * Pulls a message from the {@link AmqpsIotHubConnection} if there is one to receive. * @return the first received/unconsumed message. Null if there are no messages to consume. * @throws IOException if the {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. */ public Message consumeMessage() throws IOException { Message message = null; // Codes_SRS_AMQPSIOTHUBCONNECTION_14_019: [The function shall attempt to remove a message from the queue.] if (this.receivedMessageQueue.size() > 0) { message = this.receivedMessageQueue.remove(); this.lastMessage = (AmqpsMessage) message; } // Codes_SRS_AMQPSIOTHUBCONNECTION_14_020: [The function shall return the message if one was pulled from the queue, otherwise it shall return null.] return message; } /** * Sends the message result for the previously received * message. * * @param result the message result (one of {@link IotHubMessageResult#COMPLETE}, * {@link IotHubMessageResult#ABANDON}, or {@link IotHubMessageResult#REJECT}). * * @throws IllegalStateException if {@code sendMessageResult} is called before * {@link #consumeMessage()} is called and has returned a {@link Message}. * @throws IOException if the IoT Hub could not be reached or if any other exception is thrown while attempting to acknowledge. */ public void sendMessageResult(IotHubMessageResult result) throws IOException { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_024: [If the AMQPS Connection is closed, the function shall throw an IllegalStateException.] if(this.state == ReactorState.CLOSED){ throw new IllegalStateException("The AMQPS IotHub Connection is currently closed. Call open() before attempting to acknowledge a message."); } // Codes_SRS_AMQPSIOTHUBCONNECTION_14_022: [If sendMessageResult(result) is called before a message is received, the function shall throw an IllegalStateException.] if (this.lastMessage == null) { throw new IllegalStateException("Cannot send a message " + "result before a message is received."); } try { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_021: [If the message result is COMPLETE, ABANDON, or REJECT, the function shall acknowledge the last message with acknowledgement type COMPLETE, ABANDON, or REJECT respectively.] switch (result) { case COMPLETE: this.lastMessage.acknowledge(AmqpsMessage.ACK_TYPE.COMPLETE); break; case REJECT: this.lastMessage.acknowledge(AmqpsMessage.ACK_TYPE.REJECT); break; case ABANDON: this.lastMessage.acknowledge(AmqpsMessage.ACK_TYPE.ABANDON); break; default: // should never happen. throw new IllegalStateException( "Invalid IoT Hub message result."); } } catch(Exception e){ // Codes_SRS_AMQPSIOTHUBCONNECTION_14_023: [If the acknowledgement fails, the function shall throw an IOException.] throw new IOException(e); } } /** * Creates a binary message using the given content string and queues it for sending. * @param content The content string to send. * @return A {@link Boolean} {@link CompletableFuture} representing the whether the message has been sent and received. * @throws IOException If the current {@link AmqpsIotHubConnection.ReactorState} is CLOSED or if {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. */ public CompletableFuture scheduleSend(String content) throws IOException { return this.scheduleSend(content.getBytes(StandardCharsets.UTF_8)); } /** * Creates a binary message using the given content array and queues it for sending. * @param content The content byte array of the message to send. * @return A {@link Boolean} {@link CompletableFuture} representing the whether the message has been sent and received. * @throws IOException If the current {@link AmqpsIotHubConnection.ReactorState} is CLOSED or if {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. */ public CompletableFuture scheduleSend(byte[] content) throws IOException { // Codes_SRS_AMQPSSENDER_14_011: [If a messageId is not provided, the function shall create a binary message using the given content and a null messageId.] return this.scheduleSend(content, null); } /** * Creates a binary message using the given content array and messageId. Initializes and starts the Proton reactor. Sends the created message. * @param content The content byte array of the message to send. * @param messageId The messageId of the message. * @return A {@link Boolean} {@link CompletableFuture} representing the whether the message has been sent and received. * @throws IOException If the current {@link AmqpsIotHubConnection.ReactorState} is CLOSED or if {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. */ public CompletableFuture scheduleSend(byte[] content, Object messageId) throws IOException { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_029: [If the AMQPS Connection is closed, the function shall throw an IllegalStateException.] if(this.state == ReactorState.CLOSED){ throw new IllegalStateException("The AMQPS IotHub Connection is currently closed. Call open() before attempting to schedule a message."); } if(amqpsHandler != null) { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_026: [The function shall create a new CompletableFuture for the message acknowledgement.] // Codes_SRS_AMQPSIOTHUBCONNECTION_14_027: [The function shall create a new Tuple containing the CompletableFuture, message content, and message ID. // Codes_SRS_AMQPSIOTHUBCONNECTION_14_028: [The function shall acquire a lock and attempt to send the message.] CompletableFuture future = new CompletableFuture<>(); Tuple, byte[], Object> message = new Tuple(future, content, messageId); send(message); return future; } else { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_025: [If the AmqpsIotHubConnectionBaseHandler has not been initialized, the function shall throw a new IOException and exceptionally complete it’s CompletableFuture status member variable with the same exception.] this.completionStatus.completeExceptionally(new IOException("The Handler has not been initialized. Call open before sending.")); throw new IOException("The Handler has not been initialized. Call open before sending."); } } //============================================================================== //Protected Methods //============================================================================== /** * This blocking function attempts to send the message passed in as a parameter. * This {@link AmqpsIotHubConnection} handles all calls to this method. * *

* Only the first call to this method will result in an attempt to send. Until the message has been sent, all other * calls to this method will block. Once a message has been sent and this method notified that it has been sent, * this method will be invoked again if was previously another call to send a message. * * If a message has been passed down to the handler for sending but the message isn't sent after a default constant number of * seconds, the {@link AmqpsTransport} will set an ERROR status code on the message and it will placed back onto the queue. *

* @throws IOException If {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. */ protected synchronized void send(Tuple, byte[], Object> message) throws IOException { if(this.state == ReactorState.CLOSED) { throw new IllegalStateException("The AMQPS IotHub Connection is currently closed. Call open() before attempting to send a message."); } if(message != null) { if (this.inProgressMessageMap.size() >= this.maxQueueSize * 0.9) { message.V1.completeExceptionally(new Throwable("Insufficient link credit to send message.")); } else { try { //Use the content and ID fields of the input message to have the handler create and send the message CompletableFuture deliveryFuture = amqpsHandler.createBinaryMessage(message.V2, message.V3); //Wait for a period of time before rejecting the message new Thread(() -> { try { Thread.sleep(DEFAULT_DELIVERY_WAIT_TIME_SECONDS * 1000); deliveryFuture.completeExceptionally(new Throwable("Default timeout exceeded before this message was sent.")); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); //Wait for the deliveryFuture to be completed, providing the delivery hash code. //When this future completes, the message has been SENT Integer deliveryHash = deliveryFuture.get(); inProgressMessageMap.put(deliveryHash, message); } // There was a problem sending, exceptionally complete the future causing the message to be put back on the queue. catch (Exception e) { if (message != null) { message.V1.completeExceptionally(e); } this.fail(e); } } } else { throw new IOException("Cannot send an unitialized message."); } } /** * Informs the {@link AmqpsTransport} that the message corresponding to the {@link org.apache.qpid.proton.engine.Delivery} * {@code hash} has or has not been successfully received by the service using the boolean {@code result}. * * @param hash The hash code of the {@link org.apache.qpid.proton.engine.Delivery} corresponding to the message. * @param result The boolean result status of the sent message. */ protected synchronized void acknowledge(int hash, boolean result){ Tuple, byte[], Object> item = inProgressMessageMap.remove(hash); item.V1.complete(new Boolean(result)); //If the message queue has been successfully exhausted, complete the completionStatus future successfully. } /** * Adds a message to the received message queue. * @param m The received message. */ protected void addMessage(Message m) { this.receivedMessageQueue.add(m); } /** * Invalidates the {@link Reactor} member variable. */ protected void freeReactor(){ this.iotHubReactor = null; } /** * Invalidates the {@link AmqpsIotHubConnectionBaseHandler} member variable. */ protected void freeHandler(){ this.amqpsHandler = null; } /** * */ protected void clearInProgressMap(){ for (Tuple, byte[], Object> item : this.inProgressMessageMap.values()) { item.V1.completeExceptionally(new Throwable("Connection closed before message acknowledgement received.")); } this.inProgressMessageMap.clear(); } /** * Every {@link AmqpsIotHubConnection} has a {@link CompletableFuture} {@code completionStatus} variable. This can be used by the {@link AmqpsTransport} * to know when/if the {@link AmqpsIotHubConnection} has completed work successfully or not. * *

* The {@code completionStatus} variable is completed whenever the {@link AmqpsIotHubConnection} has exhausted the current message * queue successfully or whenever there is an error sending a message. *

* @return The boolean value of the completion status. * @throws ExecutionException If this future completed exceptionally * @throws InterruptedException If the current thread was interrupted */ protected boolean getCompletionStatus() throws ExecutionException, InterruptedException { boolean value = this.completionStatus.get().booleanValue(); return value; } /** * Waits if necessary and returns once the reactor is ready. * @throws ExecutionException If this future completed exceptionally * @throws InterruptedException If the current thread was interrupted */ protected void reactorReady() throws ExecutionException, InterruptedException{ this.reactorReady.get(); } /** * Completes the {@link CompletableFuture} {@code completionStatus} variable with an exception using the given string. * @param s The string content to place in the thrown exception. */ protected void fail(String s) { //TODO: Should also exceptionally complete anything currently in the queue that hasn't been sent. this.completionStatus = new CompletableFuture<>(); this.completionStatus.completeExceptionally(new Throwable(s)); } protected void fail(Throwable t){ this.completionStatus = new CompletableFuture<>(); this.completionStatus.completeExceptionally(t); } //============================================================================== //Private Methods //============================================================================== /** * Asynchronously runs the Proton {@link Reactor} accepting and sending messages. Any other call to this method on this * {@link AmqpsIotHubConnection} will block until the {@link Reactor} terminates and this {@link AmqpsIotHubConnection} closes. * Normally, this method should only be called once by the {@link #open()} method until the {@link AmqpsIotHubConnection} has been closed. * @throws IOException if the {@link AmqpsIotHubConnectionBaseHandler} has not been initialized. * @throws InterruptedException if there is a problem acquiring the semaphore. */ private synchronized void startReactorAsync() throws IOException, InterruptedException { //Acquire permit to continue execution of this method and spawn a new thread. reactorSemaphore.acquire(); if(this.amqpsHandler != null) { Reactor reactor = Proton.reactor(this); this.iotHubReactor = new IotHubReactor(reactor); new Thread(() -> { try { iotHubReactor.run(); //Release the semaphore and make permit available allowing for the next reactor thread to spawn. reactorSemaphore.release(); } catch(Exception e) { reactorSemaphore.release(); } }).start(); } else { throw new IOException("The Handler has not been initialized. Ensure that the AmqpsIotHubConnection is in an OPEN state by calling open()."); } } //============================================================================== //Reactor Overrides //============================================================================== /** * Event handler for reactor init event. * @param event Proton Event object */ @Override public void onReactorInit(Event event){ // Codes_SRS_AMQPSIOTHUBCONNECTION_14_030: [The event handler shall set the member AmqpsIotHubConnectionBaseHandler object to handle the connection events.] event.getReactor().connection(this.amqpsHandler); } //============================================================================== //Private Classes //============================================================================== private class Tuple{ public T1 V1; public T2 V2; public T3 V3; public Tuple(T1 V1, T2 V2, T3 V3){ this.V1 = V1; this.V2 = V2; this.V3 = V3; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy