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

com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient Maven / Gradle / Ivy

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.messaging.servicebus;

import com.azure.core.amqp.AmqpRetryPolicy;
import com.azure.core.amqp.AmqpTransaction;
import com.azure.core.amqp.exception.AmqpException;
import com.azure.core.amqp.implementation.MessageSerializer;
import com.azure.core.amqp.implementation.RetryUtil;
import com.azure.core.amqp.implementation.StringUtil;
import com.azure.core.amqp.implementation.TracerProvider;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.util.BinaryData;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.IterableStream;
import com.azure.core.util.logging.ClientLogger;
import com.azure.messaging.servicebus.ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder;
import com.azure.messaging.servicebus.implementation.DispositionStatus;
import com.azure.messaging.servicebus.implementation.LockContainer;
import com.azure.messaging.servicebus.implementation.MessagingEntityType;
import com.azure.messaging.servicebus.implementation.ServiceBusConnectionProcessor;
import com.azure.messaging.servicebus.implementation.ServiceBusReceiveLink;
import com.azure.messaging.servicebus.implementation.ServiceBusReceiveLinkProcessor;
import com.azure.messaging.servicebus.models.AbandonOptions;
import com.azure.messaging.servicebus.models.CompleteOptions;
import com.azure.messaging.servicebus.models.DeadLetterOptions;
import com.azure.messaging.servicebus.models.DeferOptions;
import com.azure.messaging.servicebus.models.ServiceBusReceiveMode;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import static com.azure.core.util.FluxUtil.fluxError;
import static com.azure.core.util.FluxUtil.monoError;
import static com.azure.messaging.servicebus.implementation.Messages.INVALID_OPERATION_DISPOSED_RECEIVER;

/**
 * An asynchronous receiver responsible for receiving {@link ServiceBusReceivedMessage messages} from a specific
 * queue or topic subscription.
 *
 * 

Create an instance of receiver

*
 * // The required parameters is connectionString, a way to authenticate with Service Bus using credentials.
 * // The connectionString/queueName must be set by the application. The 'connectionString' format is shown below.
 * // "Endpoint={fully-qualified-namespace};SharedAccessKeyName={policy-name};SharedAccessKey={key}"
 * 
 * ServiceBusReceiverAsyncClient consumer = new ServiceBusClientBuilder()
 *     .connectionString(connectionString)
 *     .receiver()
 *     .queueName(queueName)
 *     .buildAsyncClient();
 * 
* *

Create an instance of receiver using default credential

*
 * // The required parameters is connectionString, a way to authenticate with Service Bus using credentials.
 * ServiceBusReceiverAsyncClient receiver = new ServiceBusClientBuilder()
 *     .credential("<<fully-qualified-namespace>>",
 *         new DefaultAzureCredentialBuilder().build())
 *     .receiver()
 *     .queueName("<< QUEUE NAME >>")
 *     .buildAsyncClient();
 * 
* *

Receive all messages from Service Bus resource

*

This returns an infinite stream of messages from Service Bus. The stream ends when the subscription is disposed * or other terminal scenarios. See {@link #receiveMessages()} for more information.

*
 * Disposable subscription = receiver.receiveMessages()
 *     .subscribe(message -> {
 *         System.out.printf("Received Seq #: %s%n", message.getSequenceNumber());
 *         System.out.printf("Contents of message as string: %s%n", message.getBody());
 *     },
 *         error -> System.out.println("Error occurred: " + error),
 *         () -> System.out.println("Receiving complete."));
 * 
 * // When program ends, or you're done receiving all messages.
 * subscription.dispose();
 * receiver.close();
 * 
* *

Receive messages in {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode from a Service Bus * entity

*
 * // Keep a reference to `subscription`. When the program is finished receiving messages, call
 * // subscription.dispose(). This will stop fetching messages from the Service Bus.
 * Disposable subscription = receiver.receiveMessages()
 *     .subscribe(message -> {
 *         System.out.printf("Received Seq #: %s%n", message.getSequenceNumber());
 *         System.out.printf("Contents of message as string: %s%n", message.getBody().toString());
 *     }, error -> System.err.print(error));
 * 
* *

Receive messages from a specific session

*

To fetch messages from a specific session, switch to {@link ServiceBusSessionReceiverClientBuilder} and * build the session receiver client. Use {@link ServiceBusSessionReceiverAsyncClient#acceptSession(String)} to create * a session-bound {@link ServiceBusReceiverAsyncClient}. *

*
 * // The connectionString/queueName must be set by the application. The 'connectionString' format is shown below.
 * // "Endpoint={fully-qualified-namespace};SharedAccessKeyName={policy-name};SharedAccessKey={key}"
 * ServiceBusSessionReceiverAsyncClient sessionReceiver = new ServiceBusClientBuilder()
 *     .connectionString(connectionString)
 *     .sessionReceiver()
 *     .queueName(queueName)
 *     .buildAsyncClient();
 * 
 * // acceptSession(String) completes successfully with a receiver when "<< my-session-id >>" session is
 * // successfully locked.
 * // `Flux.usingWhen` is used so we dispose of the receiver resource after `receiveMessages()` completes.
 * // `Mono.usingWhen` can also be used if the resource closure only returns a single item.
 * Flux<ServiceBusReceivedMessage> sessionMessages = Flux.usingWhen(
 *     sessionReceiver.acceptSession("<< my-session-id >>"),
 *     receiver -> receiver.receiveMessages(),
 *     receiver -> Mono.fromRunnable(() -> receiver.close()));
 * 
 * // When program ends, or you're done receiving all messages, the `subscription` can be disposed of. This code
 * // is non-blocking and kicks off the operation.
 * Disposable subscription = sessionMessages.subscribe(
 *     message -> System.out.printf("Received Sequence #: %s. Contents: %s%n",
 *         message.getSequenceNumber(), message.getBody()),
 *     error -> System.err.print(error));
 * 
* *

Receive messages from the first available session

*

To process messages from the first available session, switch to {@link ServiceBusSessionReceiverClientBuilder} * and build the session receiver client. Use * {@link ServiceBusSessionReceiverAsyncClient#acceptNextSession() acceptNextSession()} to find the first available * session to process messages from.

*
 * // The connectionString/queueName must be set by the application. The 'connectionString' format is shown below.
 * // "Endpoint={fully-qualified-namespace};SharedAccessKeyName={policy-name};SharedAccessKey={key}"
 * ServiceBusSessionReceiverAsyncClient sessionReceiver = new ServiceBusClientBuilder()
 *     .connectionString(connectionString)
 *     .sessionReceiver()
 *     .queueName(queueName)
 *     .buildAsyncClient();
 * 
 * // acceptNextSession() completes successfully with a receiver when it acquires the next available session.
 * // `Flux.usingWhen` is used so we dispose of the receiver resource after `receiveMessages()` completes.
 * // `Mono.usingWhen` can also be used if the resource closure only returns a single item.
 * Flux<ServiceBusReceivedMessage> sessionMessages = Flux.usingWhen(
 *     sessionReceiver.acceptNextSession(),
 *     receiver -> receiver.receiveMessages(),
 *     receiver -> Mono.fromRunnable(() -> receiver.close()));
 * 
 * // When program ends, or you're done receiving all messages, the `subscription` can be disposed of. This code
 * // is non-blocking and kicks off the operation.
 * Disposable subscription = sessionMessages.subscribe(
 *     message -> System.out.printf("Received Sequence #: %s. Contents: %s%n",
 *         message.getSequenceNumber(), message.getBody()),
 *     error -> System.err.print(error));
 * 
* *

Rate limiting consumption of messages from a Service Bus entity

*

For message receivers that need to limit the number of messages they receive at a given time, they can use * {@link BaseSubscriber#request(long)}.

*
 * receiver.receiveMessages().subscribe(new BaseSubscriber<ServiceBusReceivedMessage>() {
 *     private static final int NUMBER_OF_MESSAGES = 5;
 *     private final AtomicInteger currentNumberOfMessages = new AtomicInteger();
 * 
 *     {@literal @}Override
 *     protected void hookOnSubscribe(Subscription subscription) {
 *         // Tell the Publisher we only want 5 message at a time.
 *         request(NUMBER_OF_MESSAGES);
 *     }
 * 
 *     {@literal @}Override
 *     protected void hookOnNext(ServiceBusReceivedMessage message) {
 *         // Process the ServiceBusReceivedMessage
 *         // If the number of messages we have currently received is a multiple of 5, that means we have reached
 *         // the last message the Subscriber will provide to us. Invoking request(long) here, tells the Publisher
 *         // that the subscriber is ready to get more messages from upstream.
 *         if (currentNumberOfMessages.incrementAndGet() % 5 == 0) {
 *             request(NUMBER_OF_MESSAGES);
 *         }
 *     }
 * });
 * 
* * @see ServiceBusClientBuilder * @see ServiceBusReceiverClient To communicate with a Service Bus resource using a synchronous client. */ @ServiceClient(builder = ServiceBusClientBuilder.class, isAsync = true) public final class ServiceBusReceiverAsyncClient implements AutoCloseable { private static final DeadLetterOptions DEFAULT_DEAD_LETTER_OPTIONS = new DeadLetterOptions(); private static final String TRANSACTION_LINK_NAME = "coordinator"; private final LockContainer renewalContainer; private final AtomicBoolean isDisposed = new AtomicBoolean(); private final LockContainer managementNodeLocks; private final ClientLogger logger = new ClientLogger(ServiceBusReceiverAsyncClient.class); private final String fullyQualifiedNamespace; private final String entityPath; private final MessagingEntityType entityType; private final ReceiverOptions receiverOptions; private final ServiceBusConnectionProcessor connectionProcessor; private final TracerProvider tracerProvider; private final MessageSerializer messageSerializer; private final Runnable onClientClose; private final ServiceBusSessionManager sessionManager; private final Semaphore completionLock = new Semaphore(1); // Starting at -1 because that is before the beginning of the stream. private final AtomicLong lastPeekedSequenceNumber = new AtomicLong(-1); private final AtomicReference consumer = new AtomicReference<>(); /** * Creates a receiver that listens to a Service Bus resource. * * @param fullyQualifiedNamespace The fully qualified domain name for the Service Bus resource. * @param entityPath The name of the topic or queue. * @param entityType The type of the Service Bus resource. * @param receiverOptions Options when receiving messages. * @param connectionProcessor The AMQP connection to the Service Bus resource. * @param tracerProvider Tracer for telemetry. * @param messageSerializer Serializes and deserializes Service Bus messages. * @param onClientClose Operation to run when the client completes. */ ServiceBusReceiverAsyncClient(String fullyQualifiedNamespace, String entityPath, MessagingEntityType entityType, ReceiverOptions receiverOptions, ServiceBusConnectionProcessor connectionProcessor, Duration cleanupInterval, TracerProvider tracerProvider, MessageSerializer messageSerializer, Runnable onClientClose) { this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, "'fullyQualifiedNamespace' cannot be null."); this.entityPath = Objects.requireNonNull(entityPath, "'entityPath' cannot be null."); this.entityType = Objects.requireNonNull(entityType, "'entityType' cannot be null."); this.receiverOptions = Objects.requireNonNull(receiverOptions, "'receiveOptions cannot be null.'"); this.connectionProcessor = Objects.requireNonNull(connectionProcessor, "'connectionProcessor' cannot be null."); this.tracerProvider = Objects.requireNonNull(tracerProvider, "'tracerProvider' cannot be null."); this.messageSerializer = Objects.requireNonNull(messageSerializer, "'messageSerializer' cannot be null."); this.onClientClose = Objects.requireNonNull(onClientClose, "'onClientClose' cannot be null."); this.managementNodeLocks = new LockContainer<>(cleanupInterval); this.renewalContainer = new LockContainer<>(Duration.ofMinutes(2), renewal -> { logger.verbose("Closing expired renewal operation. lockToken[{}]. status[{}]. throwable[{}].", renewal.getLockToken(), renewal.getStatus(), renewal.getThrowable()); renewal.close(); }); this.sessionManager = null; } ServiceBusReceiverAsyncClient(String fullyQualifiedNamespace, String entityPath, MessagingEntityType entityType, ReceiverOptions receiverOptions, ServiceBusConnectionProcessor connectionProcessor, Duration cleanupInterval, TracerProvider tracerProvider, MessageSerializer messageSerializer, Runnable onClientClose, ServiceBusSessionManager sessionManager) { this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, "'fullyQualifiedNamespace' cannot be null."); this.entityPath = Objects.requireNonNull(entityPath, "'entityPath' cannot be null."); this.entityType = Objects.requireNonNull(entityType, "'entityType' cannot be null."); this.receiverOptions = Objects.requireNonNull(receiverOptions, "'receiveOptions cannot be null.'"); this.connectionProcessor = Objects.requireNonNull(connectionProcessor, "'connectionProcessor' cannot be null."); this.tracerProvider = Objects.requireNonNull(tracerProvider, "'tracerProvider' cannot be null."); this.messageSerializer = Objects.requireNonNull(messageSerializer, "'messageSerializer' cannot be null."); this.onClientClose = Objects.requireNonNull(onClientClose, "'onClientClose' cannot be null."); this.sessionManager = Objects.requireNonNull(sessionManager, "'sessionManager' cannot be null."); this.managementNodeLocks = new LockContainer<>(cleanupInterval); this.renewalContainer = new LockContainer<>(Duration.ofMinutes(2), renewal -> { logger.info("Closing expired renewal operation. sessionId[{}]. status[{}]. throwable[{}]", renewal.getSessionId(), renewal.getStatus(), renewal.getThrowable()); renewal.close(); }); } /** * Gets the fully qualified Service Bus namespace that the connection is associated with. This is likely similar to * {@code {yournamespace}.servicebus.windows.net}. * * @return The fully qualified Service Bus namespace that the connection is associated with. */ public String getFullyQualifiedNamespace() { return fullyQualifiedNamespace; } /** * Gets the Service Bus resource this client interacts with. * * @return The Service Bus resource this client interacts with. */ public String getEntityPath() { return entityPath; } /** * Abandons a {@link ServiceBusReceivedMessage message}. This will make the message available again for processing. * Abandoning a message will increase the delivery count on the message. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * * @return A {@link Mono} that completes when the Service Bus abandon operation completes. * * @throws NullPointerException if {@code message} is null. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be abandoned. */ public Mono abandon(ServiceBusReceivedMessage message) { return updateDisposition(message, DispositionStatus.ABANDONED, null, null, null, null); } /** * Abandons a {@link ServiceBusReceivedMessage message} updates the message's properties. This will make the * message available again for processing. Abandoning a message will increase the delivery count on the message. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * @param options The options to set while abandoning the message. * * @return A {@link Mono} that completes when the Service Bus operation finishes. * @throws NullPointerException if {@code message} or {@code options} is null. Also if * {@code transactionContext.transactionId} is null when {@code options.transactionContext} is specified. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be abandoned. */ public Mono abandon(ServiceBusReceivedMessage message, AbandonOptions options) { if (Objects.isNull(options)) { return monoError(logger, new NullPointerException("'settlementOptions' cannot be null.")); } else if (!Objects.isNull(options.getTransactionContext()) && Objects.isNull(options.getTransactionContext().getTransactionId())) { return monoError(logger, new NullPointerException( "'options.transactionContext.transactionId' cannot be null.")); } return updateDisposition(message, DispositionStatus.ABANDONED, null, null, options.getPropertiesToModify(), options.getTransactionContext()); } /** * Completes a {@link ServiceBusReceivedMessage message}. This will delete the message from the service. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * * @return A {@link Mono} that finishes when the message is completed on Service Bus. * * @throws NullPointerException if {@code message} is null. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be completed. */ public Mono complete(ServiceBusReceivedMessage message) { return updateDisposition(message, DispositionStatus.COMPLETED, null, null, null, null); } /** * Completes a {@link ServiceBusReceivedMessage message} with the given options. This will delete the message from * the service. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * @param options Options used to complete the message. * * @return A {@link Mono} that finishes when the message is completed on Service Bus. * * @throws NullPointerException if {@code message} or {@code options} is null. Also if * {@code transactionContext.transactionId} is null when {@code options.transactionContext} is specified. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be completed. */ public Mono complete(ServiceBusReceivedMessage message, CompleteOptions options) { if (Objects.isNull(options)) { return monoError(logger, new NullPointerException("'options' cannot be null.")); } else if (!Objects.isNull(options.getTransactionContext()) && Objects.isNull(options.getTransactionContext().getTransactionId())) { return monoError(logger, new NullPointerException( "'options.transactionContext.transactionId' cannot be null.")); } return updateDisposition(message, DispositionStatus.COMPLETED, null, null, null, options.getTransactionContext()); } /** * Defers a {@link ServiceBusReceivedMessage message}. This will move message into the deferred sub-queue. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * * @return A {@link Mono} that completes when the Service Bus defer operation finishes. * * @throws NullPointerException if {@code message} is null. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be deferred. * @see Message deferral */ public Mono defer(ServiceBusReceivedMessage message) { return updateDisposition(message, DispositionStatus.DEFERRED, null, null, null, null); } /** * Defers a {@link ServiceBusReceivedMessage message} with the options set. This will move message into * the deferred sub-queue. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * @param options Options used to defer the message. * * @return A {@link Mono} that completes when the defer operation finishes. * * @throws NullPointerException if {@code message} or {@code options} is null. Also if * {@code transactionContext.transactionId} is null when {@code options.transactionContext} is specified. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be deferred. * @see Message deferral */ public Mono defer(ServiceBusReceivedMessage message, DeferOptions options) { if (Objects.isNull(options)) { return monoError(logger, new NullPointerException("'options' cannot be null.")); } else if (!Objects.isNull(options.getTransactionContext()) && Objects.isNull(options.getTransactionContext().getTransactionId())) { return monoError(logger, new NullPointerException( "'options.transactionContext.transactionId' cannot be null.")); } return updateDisposition(message, DispositionStatus.DEFERRED, null, null, options.getPropertiesToModify(), options.getTransactionContext()); } /** * Moves a {@link ServiceBusReceivedMessage message} to the dead-letter sub-queue. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * * @return A {@link Mono} that completes when the dead letter operation finishes. * * @throws NullPointerException if {@code message} is null. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be dead-lettered. * @see Dead letter * queues */ public Mono deadLetter(ServiceBusReceivedMessage message) { return deadLetter(message, DEFAULT_DEAD_LETTER_OPTIONS); } /** * Moves a {@link ServiceBusReceivedMessage message} to the dead-letter sub-queue with the given options. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * @param options Options used to dead-letter the message. * * @return A {@link Mono} that completes when the dead letter operation finishes. * * @throws NullPointerException if {@code message} or {@code options} is null. Also if * {@code transactionContext.transactionId} is null when {@code options.transactionContext} is specified. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from * {@link ServiceBusReceiverAsyncClient#peekMessage() peekMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the message could not be dead-lettered. * @see Dead letter * queues */ public Mono deadLetter(ServiceBusReceivedMessage message, DeadLetterOptions options) { if (Objects.isNull(options)) { return monoError(logger, new NullPointerException("'options' cannot be null.")); } else if (!Objects.isNull(options.getTransactionContext()) && Objects.isNull(options.getTransactionContext().getTransactionId())) { return monoError(logger, new NullPointerException( "'options.transactionContext.transactionId' cannot be null.")); } return updateDisposition(message, DispositionStatus.SUSPENDED, options.getDeadLetterReason(), options.getDeadLetterErrorDescription(), options.getPropertiesToModify(), options.getTransactionContext()); } /** * Gets the state of the session if this receiver is a session receiver. * * @return The session state or an empty Mono if there is no state set for the session. * @throws IllegalStateException if the receiver is a non-session receiver or receiver is already closed. * @throws ServiceBusException if the session state could not be acquired. */ public Mono getSessionState() { return getSessionState(receiverOptions.getSessionId()); } /** * Reads the next active message without changing the state of the receiver or the message source. The first call to * {@code peek()} fetches the first active message for this receiver. Each subsequent call fetches the subsequent * message in the entity. * * @return A peeked {@link ServiceBusReceivedMessage}. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if an error occurs while peeking at the message. * @see Message browsing */ public Mono peekMessage() { return peekMessage(receiverOptions.getSessionId()); } /** * Reads the next active message without changing the state of the receiver or the message source. The first call to * {@code peek()} fetches the first active message for this receiver. Each subsequent call fetches the subsequent * message in the entity. * * @param sessionId Session id of the message to peek from. {@code null} if there is no session. * * @return A peeked {@link ServiceBusReceivedMessage}. * @throws IllegalStateException if the receiver is disposed. * @see Message browsing */ Mono peekMessage(String sessionId) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "peek"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(channel -> { final long sequence = lastPeekedSequenceNumber.get() + 1; logger.verbose("Peek message from sequence number: {}", sequence); return channel.peek(sequence, sessionId, getLinkName(sessionId)); }) .handle((message, sink) -> { final long current = lastPeekedSequenceNumber .updateAndGet(value -> Math.max(value, message.getSequenceNumber())); logger.verbose("Updating last peeked sequence number: {}", current); sink.next(message); }); } /** * Starting from the given sequence number, reads next the active message without changing the state of the receiver * or the message source. * * @param sequenceNumber The sequence number from where to read the message. * * @return A peeked {@link ServiceBusReceivedMessage}. * * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if an error occurs while peeking at the message. * @see Message browsing */ public Mono peekMessage(long sequenceNumber) { return peekMessage(sequenceNumber, receiverOptions.getSessionId()); } /** * Starting from the given sequence number, reads next the active message without changing the state of the receiver * or the message source. * * @param sequenceNumber The sequence number from where to read the message. * @param sessionId Session id of the message to peek from. {@code null} if there is no session. * * @return A peeked {@link ServiceBusReceivedMessage}. * @throws IllegalStateException if receiver is already disposed. * @see Message browsing */ Mono peekMessage(long sequenceNumber, String sessionId) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "peekAt"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(node -> node.peek(sequenceNumber, sessionId, getLinkName(sessionId))); } /** * Reads the next batch of active messages without changing the state of the receiver or the message source. * * @param maxMessages The number of messages. * * @return A {@link Flux} of {@link ServiceBusReceivedMessage messages} that are peeked. * * @throws IllegalArgumentException if {@code maxMessages} is not a positive integer. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if an error occurs while peeking at messages. * @see Message browsing */ public Flux peekMessages(int maxMessages) { return peekMessages(maxMessages, receiverOptions.getSessionId()); } /** * Reads the next batch of active messages without changing the state of the receiver or the message source. * * @param maxMessages The number of messages. * @param sessionId Session id of the messages to peek from. {@code null} if there is no session. * * @return An {@link IterableStream} of {@link ServiceBusReceivedMessage messages} that are peeked. * @throws IllegalArgumentException if {@code maxMessages} is not a positive integer. * @throws IllegalStateException if receiver is already disposed. * @see Message browsing */ Flux peekMessages(int maxMessages, String sessionId) { if (isDisposed.get()) { return fluxError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "peekBatch"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMapMany(node -> { final long nextSequenceNumber = lastPeekedSequenceNumber.get() + 1; logger.verbose("Peek batch from sequence number: {}", nextSequenceNumber); final Flux messages = node.peek(nextSequenceNumber, sessionId, getLinkName(sessionId), maxMessages); // To prevent it from throwing NoSuchElementException in .last(), we produce an empty message with // the same sequence number. final Mono handle = messages .switchIfEmpty(Mono.fromCallable(() -> { ServiceBusReceivedMessage emptyMessage = new ServiceBusReceivedMessage(BinaryData .fromBytes(new byte[0])); emptyMessage.setSequenceNumber(lastPeekedSequenceNumber.get()); return emptyMessage; })) .last() .handle((last, sink) -> { final long current = lastPeekedSequenceNumber .updateAndGet(value -> Math.max(value, last.getSequenceNumber())); logger.verbose("Last peeked sequence number in batch: {}", current); sink.complete(); }); return Flux.merge(messages, handle); }); } /** * Starting from the given sequence number, reads the next batch of active messages without changing the state of * the receiver or the message source. * * @param maxMessages The number of messages. * @param sequenceNumber The sequence number from where to start reading messages. * * @return A {@link Flux} of {@link ServiceBusReceivedMessage messages} peeked. * * @throws IllegalArgumentException if {@code maxMessages} is not a positive integer. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if an error occurs while peeking at messages. * @see Message browsing */ public Flux peekMessages(int maxMessages, long sequenceNumber) { return peekMessages(maxMessages, sequenceNumber, receiverOptions.getSessionId()); } /** * Starting from the given sequence number, reads the next batch of active messages without changing the state of * the receiver or the message source. * * @param maxMessages The number of messages. * @param sequenceNumber The sequence number from where to start reading messages. * @param sessionId Session id of the messages to peek from. {@code null} if there is no session. * * @return An {@link IterableStream} of {@link ServiceBusReceivedMessage} peeked. * @throws IllegalArgumentException if {@code maxMessages} is not a positive integer. * @throws IllegalStateException if receiver is already disposed. * @see Message browsing */ Flux peekMessages(int maxMessages, long sequenceNumber, String sessionId) { if (isDisposed.get()) { return fluxError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "peekBatchAt"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMapMany(node -> node.peek(sequenceNumber, sessionId, getLinkName(sessionId), maxMessages)); } /** * Receives an infinite stream of {@link ServiceBusReceivedMessage messages} from the Service Bus entity. * This Flux continuously receives messages from a Service Bus entity until either: * *
    *
  • The receiver is closed.
  • *
  • The subscription to the Flux is disposed.
  • *
  • A terminal signal from a downstream subscriber is propagated upstream (ie. {@link Flux#take(long)} or * {@link Flux#take(Duration)}).
  • *
  • An {@link AmqpException} occurs that causes the receive link to stop.
  • *
* * @return An infinite stream of messages from the Service Bus entity. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if an error occurs while receiving messages. */ public Flux receiveMessages() { if (isDisposed.get()) { return fluxError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "receiveMessages"))); } // Without limitRate(), if the user calls receiveMessages().subscribe(), it will call // ServiceBusReceiveLinkProcessor.request(long request) where request = Long.MAX_VALUE. // We turn this one-time non-backpressure request to continuous requests with backpressure. // If receiverOptions.prefetchCount is set to non-zero, it will be passed to ServiceBusReceiveLinkProcessor // to auto-refill the prefetch buffer. A request will retrieve one message from this buffer. // If receiverOptions.prefetchCount is 0 (default value), // the request will add a link credit so one message is retrieved from the service. return receiveMessagesNoBackPressure().limitRate(1, 0); } Flux receiveMessagesNoBackPressure() { return receiveMessagesWithContext(0) .handle((serviceBusMessageContext, sink) -> { if (serviceBusMessageContext.hasError()) { sink.error(serviceBusMessageContext.getThrowable()); return; } sink.next(serviceBusMessageContext.getMessage()); }); } /** * Receives an infinite stream of {@link ServiceBusReceivedMessage messages} from the Service Bus entity. * This Flux continuously receives messages from a Service Bus entity until either: * *
    *
  • The receiver is closed.
  • *
  • The subscription to the Flux is disposed.
  • *
  • A terminal signal from a downstream subscriber is propagated upstream (ie. {@link Flux#take(long)} or * {@link Flux#take(Duration)}).
  • *
  • An {@link AmqpException} occurs that causes the receive link to stop.
  • *
* * @return An infinite stream of messages from the Service Bus entity. */ Flux receiveMessagesWithContext() { return receiveMessagesWithContext(1); } Flux receiveMessagesWithContext(int highTide) { final Flux messageFlux = sessionManager != null ? sessionManager.receive() : getOrCreateConsumer().receive().map(ServiceBusMessageContext::new); final Flux withAutoLockRenewal; if (receiverOptions.isAutoLockRenewEnabled()) { withAutoLockRenewal = new FluxAutoLockRenew(messageFlux, receiverOptions.getMaxLockRenewDuration(), renewalContainer, this::renewMessageLock); } else { withAutoLockRenewal = messageFlux; } Flux result; if (receiverOptions.isEnableAutoComplete()) { result = new FluxAutoComplete(withAutoLockRenewal, completionLock, context -> context.getMessage() != null ? complete(context.getMessage()) : Mono.empty(), context -> context.getMessage() != null ? abandon(context.getMessage()) : Mono.empty()); } else { result = withAutoLockRenewal; } if (highTide > 0) { result = result.limitRate(highTide, 0); } return result .onErrorMap(throwable -> mapError(throwable, ServiceBusErrorSource.RECEIVE)); } /** * Receives a deferred {@link ServiceBusReceivedMessage message}. Deferred messages can only be received by using * sequence number. * * @param sequenceNumber The {@link ServiceBusReceivedMessage#getSequenceNumber() sequence number} of the * message. * * @return A deferred message with the matching {@code sequenceNumber}. * * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if deferred message cannot be received. */ public Mono receiveDeferredMessage(long sequenceNumber) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "receiveDeferredMessage"))); } return receiveDeferredMessage(sequenceNumber, receiverOptions.getSessionId()); } /** * Receives a deferred {@link ServiceBusReceivedMessage message}. Deferred messages can only be received by using * sequence number. * * @param sequenceNumber The {@link ServiceBusReceivedMessage#getSequenceNumber() sequence number} of the * message. * @param sessionId Session id of the deferred message. {@code null} if there is no session. * * @return A deferred message with the matching {@code sequenceNumber}. * @throws IllegalStateException if receiver is already disposed. */ Mono receiveDeferredMessage(long sequenceNumber, String sessionId) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "receiveDeferredMessage"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(node -> node.receiveDeferredMessages(receiverOptions.getReceiveMode(), sessionId, getLinkName(sessionId), Collections.singleton(sequenceNumber)).last()) .map(receivedMessage -> { if (CoreUtils.isNullOrEmpty(receivedMessage.getLockToken())) { return receivedMessage; } if (receiverOptions.getReceiveMode() == ServiceBusReceiveMode.PEEK_LOCK) { receivedMessage.setLockedUntil(managementNodeLocks.addOrUpdate(receivedMessage.getLockToken(), receivedMessage.getLockedUntil(), receivedMessage.getLockedUntil())); } return receivedMessage; }); } /** * Receives a batch of deferred {@link ServiceBusReceivedMessage messages}. Deferred messages can only be received * by using sequence number. * * @param sequenceNumbers The sequence numbers of the deferred messages. * * @return A {@link Flux} of deferred {@link ServiceBusReceivedMessage messages}. * * @throws NullPointerException if {@code sequenceNumbers} is null. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if deferred messages cannot be received. */ public Flux receiveDeferredMessages(Iterable sequenceNumbers) { return receiveDeferredMessages(sequenceNumbers, receiverOptions.getSessionId()); } /** * Receives a batch of deferred {@link ServiceBusReceivedMessage messages}. Deferred messages can only be received * by using sequence number. * * @param sequenceNumbers The sequence numbers of the deferred messages. * @param sessionId Session id of the deferred messages. {@code null} if there is no session. * * @return An {@link IterableStream} of deferred {@link ServiceBusReceivedMessage messages}. * @throws IllegalStateException if receiver is already disposed. */ Flux receiveDeferredMessages(Iterable sequenceNumbers, String sessionId) { if (isDisposed.get()) { return fluxError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "receiveDeferredMessageBatch"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMapMany(node -> node.receiveDeferredMessages(receiverOptions.getReceiveMode(), sessionId, getLinkName(sessionId), sequenceNumbers)) .map(receivedMessage -> { if (CoreUtils.isNullOrEmpty(receivedMessage.getLockToken())) { return receivedMessage; } if (receiverOptions.getReceiveMode() == ServiceBusReceiveMode.PEEK_LOCK) { receivedMessage.setLockedUntil(managementNodeLocks.addOrUpdate(receivedMessage.getLockToken(), receivedMessage.getLockedUntil(), receivedMessage.getLockedUntil())); } return receivedMessage; }); } /** * Asynchronously renews the lock on the message. The lock will be renewed based on the setting specified on the * entity. When a message is received in {@link ServiceBusReceiveMode#PEEK_LOCK} mode, the message is locked on the * server for this receiver instance for a duration as specified during the entity creation (LockDuration). If * processing of the message requires longer than this duration, the lock needs to be renewed. For each renewal, the * lock is reset to the entity's LockDuration value. * * @param message The {@link ServiceBusReceivedMessage} to perform auto-lock renewal. * * @return The new expiration time for the message. * * @throws NullPointerException if {@code message} or {@code message.getLockToken()} is null. * @throws UnsupportedOperationException if the receiver was opened in * {@link ServiceBusReceiveMode#RECEIVE_AND_DELETE} mode or if the message was received from peekMessage. * @throws IllegalStateException if the receiver is a session receiver or receiver is already disposed. * @throws IllegalArgumentException if {@code message.getLockToken()} is an empty value. */ public Mono renewMessageLock(ServiceBusReceivedMessage message) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "renewMessageLock"))); } else if (Objects.isNull(message)) { return monoError(logger, new NullPointerException("'message' cannot be null.")); } else if (Objects.isNull(message.getLockToken())) { return monoError(logger, new NullPointerException("'message.getLockToken()' cannot be null.")); } else if (message.getLockToken().isEmpty()) { return monoError(logger, new IllegalArgumentException("'message.getLockToken()' cannot be empty.")); } else if (receiverOptions.isSessionReceiver()) { final String errorMessage = "Renewing message lock is an invalid operation when working with sessions."; return monoError(logger, new IllegalStateException(errorMessage)); } return renewMessageLock(message.getLockToken()) .onErrorMap(throwable -> mapError(throwable, ServiceBusErrorSource.RENEW_LOCK)); } /** * Asynchronously renews the lock on the message. The lock will be renewed based on the setting specified on the * entity. * * @param lockToken to be renewed. * * @return The new expiration time for the message. * @throws IllegalStateException if receiver is already disposed. */ Mono renewMessageLock(String lockToken) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "renewMessageLock"))); } return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(serviceBusManagementNode -> serviceBusManagementNode.renewMessageLock(lockToken, getLinkName(null))) .map(offsetDateTime -> managementNodeLocks.addOrUpdate(lockToken, offsetDateTime, offsetDateTime)); } /** * Starts the auto lock renewal for a {@link ServiceBusReceivedMessage message}. * * @param message The {@link ServiceBusReceivedMessage} to perform this operation. * @param maxLockRenewalDuration Maximum duration to keep renewing the lock token. * * @return A Mono that completes when the message renewal operation has completed up until * {@code maxLockRenewalDuration}. * * @throws NullPointerException if {@code message}, {@code message.getLockToken()}, or * {@code maxLockRenewalDuration} is null. * @throws IllegalStateException if the receiver is a session receiver or the receiver is disposed. * @throws IllegalArgumentException if {@code message.getLockToken()} is an empty value. * @throws ServiceBusException If the message lock cannot be renewed. */ public Mono renewMessageLock(ServiceBusReceivedMessage message, Duration maxLockRenewalDuration) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "getAutoRenewMessageLock"))); } else if (Objects.isNull(message)) { return monoError(logger, new NullPointerException("'message' cannot be null.")); } else if (Objects.isNull(message.getLockToken())) { return monoError(logger, new NullPointerException("'message.getLockToken()' cannot be null.")); } else if (message.getLockToken().isEmpty()) { return monoError(logger, new IllegalArgumentException("'message.getLockToken()' cannot be empty.")); } else if (receiverOptions.isSessionReceiver()) { return monoError(logger, new IllegalStateException( String.format("Cannot renew message lock [%s] for a session receiver.", message.getLockToken()))); } else if (maxLockRenewalDuration == null) { return monoError(logger, new NullPointerException("'maxLockRenewalDuration' cannot be null.")); } else if (maxLockRenewalDuration.isNegative()) { return monoError(logger, new IllegalArgumentException("'maxLockRenewalDuration' cannot be negative.")); } final LockRenewalOperation operation = new LockRenewalOperation(message.getLockToken(), maxLockRenewalDuration, false, ignored -> renewMessageLock(message)); renewalContainer.addOrUpdate(message.getLockToken(), OffsetDateTime.now().plus(maxLockRenewalDuration), operation); return operation.getCompletionOperation() .onErrorMap(throwable -> mapError(throwable, ServiceBusErrorSource.RENEW_LOCK)); } /** * Renews the session lock if this receiver is a session receiver. * * @return The next expiration time for the session lock. * @throws IllegalStateException if the receiver is a non-session receiver or if receiver is already disposed. * @throws ServiceBusException if the session lock cannot be renewed. */ public Mono renewSessionLock() { return renewSessionLock(receiverOptions.getSessionId()); } /** * Starts the auto lock renewal for the session this receiver works for. * * @param maxLockRenewalDuration Maximum duration to keep renewing the session lock. * * @return A lock renewal operation for the message. * * @throws NullPointerException if {@code maxLockRenewalDuration} is null. * @throws IllegalStateException if the receiver is a non-session receiver or the receiver is disposed. * @throws ServiceBusException if the session lock renewal operation cannot be started. */ public Mono renewSessionLock(Duration maxLockRenewalDuration) { return this.renewSessionLock(receiverOptions.getSessionId(), maxLockRenewalDuration); } /** * Sets the state of the session this receiver works for. * * @param sessionState State to set on the session. * * @return A Mono that completes when the session is set * @throws IllegalStateException if the receiver is a non-session receiver or receiver is already disposed. * @throws ServiceBusException if the session state cannot be set. */ public Mono setSessionState(byte[] sessionState) { return this.setSessionState(receiverOptions.getSessionId(), sessionState); } /** * Starts a new service side transaction. The {@link ServiceBusTransactionContext transaction context} should be * passed to all operations that needs to be in this transaction. * *

Creating and using a transaction

*
     * // This mono creates a transaction and caches the output value, so we can associate operations with the
     * // transaction. It does not cache the value if it is an error or completes with no items, effectively retrying
     * // the operation.
     * Mono<ServiceBusTransactionContext> transactionContext = receiver.createTransaction()
     *     .cache(value -> Duration.ofMillis(Long.MAX_VALUE),
     *         error -> Duration.ZERO,
     *         () -> Duration.ZERO);
     * 
     * transactionContext.flatMap(transaction -> {
     *     // Process messages and associate operations with the transaction.
     *     Mono<Void> operations = Mono.when(
     *         receiver.receiveDeferredMessage(sequenceNumber).flatMap(message ->
     *             receiver.complete(message, new CompleteOptions().setTransactionContext(transaction))),
     *         receiver.abandon(receivedMessage, new AbandonOptions().setTransactionContext(transaction)));
     * 
     *     // Finally, either commit or rollback the transaction once all the operations are associated with it.
     *     return operations.flatMap(transactionOperations -> receiver.commitTransaction(transaction));
     * });
     * 
* * @return The {@link Mono} that finishes this operation on service bus resource. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if a transaction cannot be created. */ public Mono createTransaction() { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "createTransaction"))); } return connectionProcessor .flatMap(connection -> connection.createSession(TRANSACTION_LINK_NAME)) .flatMap(transactionSession -> transactionSession.createTransaction()) .map(transaction -> new ServiceBusTransactionContext(transaction.getTransactionId())); } /** * Commits the transaction and all the operations associated with it. * *

Creating and using a transaction

*
     * // This mono creates a transaction and caches the output value, so we can associate operations with the
     * // transaction. It does not cache the value if it is an error or completes with no items, effectively retrying
     * // the operation.
     * Mono<ServiceBusTransactionContext> transactionContext = receiver.createTransaction()
     *     .cache(value -> Duration.ofMillis(Long.MAX_VALUE),
     *         error -> Duration.ZERO,
     *         () -> Duration.ZERO);
     * 
     * transactionContext.flatMap(transaction -> {
     *     // Process messages and associate operations with the transaction.
     *     Mono<Void> operations = Mono.when(
     *         receiver.receiveDeferredMessage(sequenceNumber).flatMap(message ->
     *             receiver.complete(message, new CompleteOptions().setTransactionContext(transaction))),
     *         receiver.abandon(receivedMessage, new AbandonOptions().setTransactionContext(transaction)));
     * 
     *     // Finally, either commit or rollback the transaction once all the operations are associated with it.
     *     return operations.flatMap(transactionOperations -> receiver.commitTransaction(transaction));
     * });
     * 
* * @param transactionContext The transaction to be commit. * * @return The {@link Mono} that finishes this operation on service bus resource. * * @throws NullPointerException if {@code transactionContext} or {@code transactionContext.transactionId} is null. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the transaction could not be committed. */ public Mono commitTransaction(ServiceBusTransactionContext transactionContext) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "commitTransaction"))); } if (Objects.isNull(transactionContext)) { return monoError(logger, new NullPointerException("'transactionContext' cannot be null.")); } else if (Objects.isNull(transactionContext.getTransactionId())) { return monoError(logger, new NullPointerException("'transactionContext.transactionId' cannot be null.")); } return connectionProcessor .flatMap(connection -> connection.createSession(TRANSACTION_LINK_NAME)) .flatMap(transactionSession -> transactionSession.commitTransaction(new AmqpTransaction( transactionContext.getTransactionId()))); } /** * Rollbacks the transaction given and all operations associated with it. * *

Creating and using a transaction

*
     * // This mono creates a transaction and caches the output value, so we can associate operations with the
     * // transaction. It does not cache the value if it is an error or completes with no items, effectively retrying
     * // the operation.
     * Mono<ServiceBusTransactionContext> transactionContext = receiver.createTransaction()
     *     .cache(value -> Duration.ofMillis(Long.MAX_VALUE),
     *         error -> Duration.ZERO,
     *         () -> Duration.ZERO);
     * 
     * transactionContext.flatMap(transaction -> {
     *     // Process messages and associate operations with the transaction.
     *     Mono<Void> operations = Mono.when(
     *         receiver.receiveDeferredMessage(sequenceNumber).flatMap(message ->
     *             receiver.complete(message, new CompleteOptions().setTransactionContext(transaction))),
     *         receiver.abandon(receivedMessage, new AbandonOptions().setTransactionContext(transaction)));
     * 
     *     // Finally, either commit or rollback the transaction once all the operations are associated with it.
     *     return operations.flatMap(transactionOperations -> receiver.commitTransaction(transaction));
     * });
     * 
* * @param transactionContext The transaction to rollback. * * @return The {@link Mono} that finishes this operation on service bus resource. * @throws NullPointerException if {@code transactionContext} or {@code transactionContext.transactionId} is null. * @throws IllegalStateException if receiver is already disposed. * @throws ServiceBusException if the transaction could not be rolled back. */ public Mono rollbackTransaction(ServiceBusTransactionContext transactionContext) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "rollbackTransaction"))); } if (Objects.isNull(transactionContext)) { return monoError(logger, new NullPointerException("'transactionContext' cannot be null.")); } else if (Objects.isNull(transactionContext.getTransactionId())) { return monoError(logger, new NullPointerException("'transactionContext.transactionId' cannot be null.")); } return connectionProcessor .flatMap(connection -> connection.createSession(TRANSACTION_LINK_NAME)) .flatMap(transactionSession -> transactionSession.rollbackTransaction(new AmqpTransaction( transactionContext.getTransactionId()))); } /** * Disposes of the consumer by closing the underlying links to the service. */ @Override public void close() { if (isDisposed.get()) { return; } try { completionLock.acquire(); } catch (InterruptedException e) { logger.info("Unable to obtain completion lock.", e); } if (isDisposed.getAndSet(true)) { return; } // Blocking until the last message has been completed. logger.info("Removing receiver links."); final ServiceBusAsyncConsumer disposed = consumer.getAndSet(null); if (disposed != null) { disposed.close(); } if (sessionManager != null) { sessionManager.close(); } managementNodeLocks.close(); renewalContainer.close(); onClientClose.run(); } /** * @return receiver options set by user; */ ReceiverOptions getReceiverOptions() { return receiverOptions; } /** * Gets whether or not the management node contains the message lock token and it has not expired. Lock tokens are * held by the management node when they are received from the management node or management operations are * performed using that {@code lockToken}. * * @param lockToken Lock token to check for. * * @return {@code true} if the management node contains the lock token and false otherwise. */ private boolean isManagementToken(String lockToken) { return managementNodeLocks.containsUnexpired(lockToken); } private Mono updateDisposition(ServiceBusReceivedMessage message, DispositionStatus dispositionStatus, String deadLetterReason, String deadLetterErrorDescription, Map propertiesToModify, ServiceBusTransactionContext transactionContext) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, dispositionStatus.getValue()))); } else if (Objects.isNull(message)) { return monoError(logger, new NullPointerException("'message' cannot be null.")); } final String lockToken = message.getLockToken(); final String sessionId = message.getSessionId(); if (receiverOptions.getReceiveMode() != ServiceBusReceiveMode.PEEK_LOCK) { return Mono.error(logger.logExceptionAsError(new UnsupportedOperationException(String.format( "'%s' is not supported on a receiver opened in ReceiveMode.RECEIVE_AND_DELETE.", dispositionStatus)))); } else if (message.isSettled()) { return Mono.error(logger.logExceptionAsError( new IllegalArgumentException("The message has either been deleted or already settled."))); } else if (message.getLockToken() == null) { // message must be a peeked message (or somehow they created a message w/o a lock token) final String errorMessage = "This operation is not supported for peeked messages. " + "Only messages received using receiveMessages() in PEEK_LOCK mode can be settled."; return Mono.error( logger.logExceptionAsError(new UnsupportedOperationException(errorMessage)) ); } final String sessionIdToUse; if (sessionId == null && !CoreUtils.isNullOrEmpty(receiverOptions.getSessionId())) { sessionIdToUse = receiverOptions.getSessionId(); } else { sessionIdToUse = sessionId; } logger.verbose("{}: Update started. Disposition: {}. Lock: {}. SessionId: {}.", entityPath, dispositionStatus, lockToken, sessionIdToUse); // This operation is not kicked off until it is subscribed to. final Mono performOnManagement = connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(node -> node.updateDisposition(lockToken, dispositionStatus, deadLetterReason, deadLetterErrorDescription, propertiesToModify, sessionId, getLinkName(sessionId), transactionContext)) .then(Mono.fromRunnable(() -> { logger.info("{}: Management node Update completed. Disposition: {}. Lock: {}.", entityPath, dispositionStatus, lockToken); message.setIsSettled(); managementNodeLocks.remove(lockToken); renewalContainer.remove(lockToken); })); Mono updateDispositionOperation; if (sessionManager != null) { updateDispositionOperation = sessionManager.updateDisposition(lockToken, sessionId, dispositionStatus, propertiesToModify, deadLetterReason, deadLetterErrorDescription, transactionContext) .flatMap(isSuccess -> { if (isSuccess) { message.setIsSettled(); renewalContainer.remove(lockToken); return Mono.empty(); } logger.info("Could not perform on session manger. Performing on management node."); return performOnManagement; }); } else { final ServiceBusAsyncConsumer existingConsumer = consumer.get(); if (isManagementToken(lockToken) || existingConsumer == null) { updateDispositionOperation = performOnManagement; } else { updateDispositionOperation = existingConsumer.updateDisposition(lockToken, dispositionStatus, deadLetterReason, deadLetterErrorDescription, propertiesToModify, transactionContext) .then(Mono.fromRunnable(() -> { logger.verbose("{}: Update completed. Disposition: {}. Lock: {}.", entityPath, dispositionStatus, lockToken); message.setIsSettled(); renewalContainer.remove(lockToken); })); } } return updateDispositionOperation .onErrorMap(throwable -> { if (throwable instanceof ServiceBusException) { return throwable; } switch (dispositionStatus) { case COMPLETED: return new ServiceBusException(throwable, ServiceBusErrorSource.COMPLETE); case ABANDONED: return new ServiceBusException(throwable, ServiceBusErrorSource.ABANDON); default: return new ServiceBusException(throwable, ServiceBusErrorSource.UNKNOWN); } }); } private ServiceBusAsyncConsumer getOrCreateConsumer() { final ServiceBusAsyncConsumer existing = consumer.get(); if (existing != null) { return existing; } final String linkName = StringUtil.getRandomString(entityPath); logger.info("{}: Creating consumer for link '{}'", entityPath, linkName); final Flux receiveLink = connectionProcessor.flatMap(connection -> { if (receiverOptions.isSessionReceiver()) { return connection.createReceiveLink(linkName, entityPath, receiverOptions.getReceiveMode(), null, entityType, receiverOptions.getSessionId()); } else { return connection.createReceiveLink(linkName, entityPath, receiverOptions.getReceiveMode(), null, entityType); } }) .doOnNext(next -> { final String format = "Created consumer for Service Bus resource: [{}] mode: [{}]" + " sessionEnabled? {} transferEntityPath: [{}], entityType: [{}]"; logger.verbose(format, next.getEntityPath(), receiverOptions.getReceiveMode(), CoreUtils.isNullOrEmpty(receiverOptions.getSessionId()), "N/A", entityType); }) .repeat(); final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(connectionProcessor.getRetryOptions()); final ServiceBusReceiveLinkProcessor linkMessageProcessor = receiveLink.subscribeWith( new ServiceBusReceiveLinkProcessor(receiverOptions.getPrefetchCount(), retryPolicy, receiverOptions.getReceiveMode())); final ServiceBusAsyncConsumer newConsumer = new ServiceBusAsyncConsumer(linkName, linkMessageProcessor, messageSerializer, receiverOptions); // There could have been multiple threads trying to create this async consumer when the result was null. // If another one had set the value while we were creating this resource, dispose of newConsumer. if (consumer.compareAndSet(null, newConsumer)) { return newConsumer; } else { newConsumer.close(); return consumer.get(); } } /** * If the receiver has not connected via {@link #receiveMessages()}, all its current operations have been performed * through the management node. * * @return The name of the receive link, or null of it has not connected via a receive link. */ private String getLinkName(String sessionId) { if (sessionManager != null && !CoreUtils.isNullOrEmpty(sessionId)) { return sessionManager.getLinkName(sessionId); } else if (!CoreUtils.isNullOrEmpty(sessionId) && !receiverOptions.isSessionReceiver()) { return null; } else { final ServiceBusAsyncConsumer existing = consumer.get(); return existing != null ? existing.getLinkName() : null; } } Mono renewSessionLock(String sessionId) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "renewSessionLock"))); } else if (!receiverOptions.isSessionReceiver()) { return monoError(logger, new IllegalStateException("Cannot renew session lock on a non-session receiver.")); } final String linkName = sessionManager != null ? sessionManager.getLinkName(sessionId) : null; return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(channel -> channel.renewSessionLock(sessionId, linkName)) .onErrorMap(throwable -> mapError(throwable, ServiceBusErrorSource.RENEW_LOCK)); } Mono renewSessionLock(String sessionId, Duration maxLockRenewalDuration) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "renewSessionLock"))); } else if (!receiverOptions.isSessionReceiver()) { return monoError(logger, new IllegalStateException( "Cannot renew session lock on a non-session receiver.")); } else if (maxLockRenewalDuration == null) { return monoError(logger, new NullPointerException("'maxLockRenewalDuration' cannot be null.")); } else if (maxLockRenewalDuration.isNegative()) { return monoError(logger, new IllegalArgumentException( "'maxLockRenewalDuration' cannot be negative.")); } else if (Objects.isNull(sessionId)) { return monoError(logger, new NullPointerException("'sessionId' cannot be null.")); } else if (sessionId.isEmpty()) { return monoError(logger, new IllegalArgumentException("'sessionId' cannot be empty.")); } final LockRenewalOperation operation = new LockRenewalOperation(sessionId, maxLockRenewalDuration, true, this::renewSessionLock); renewalContainer.addOrUpdate(sessionId, OffsetDateTime.now().plus(maxLockRenewalDuration), operation); return operation.getCompletionOperation() .onErrorMap(throwable -> mapError(throwable, ServiceBusErrorSource.RENEW_LOCK)); } Mono setSessionState(String sessionId, byte[] sessionState) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "setSessionState"))); } else if (!receiverOptions.isSessionReceiver()) { return monoError(logger, new IllegalStateException("Cannot set session state on a non-session receiver.")); } final String linkName = sessionManager != null ? sessionManager.getLinkName(sessionId) : null; return connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(channel -> channel.setSessionState(sessionId, sessionState, linkName)) .onErrorMap((err) -> mapError(err, ServiceBusErrorSource.RECEIVE)); } Mono getSessionState(String sessionId) { if (isDisposed.get()) { return monoError(logger, new IllegalStateException( String.format(INVALID_OPERATION_DISPOSED_RECEIVER, "getSessionState"))); } else if (!receiverOptions.isSessionReceiver()) { return monoError(logger, new IllegalStateException("Cannot get session state on a non-session receiver.")); } Mono result; if (sessionManager != null) { result = sessionManager.getSessionState(sessionId); } else { result = connectionProcessor .flatMap(connection -> connection.getManagementNode(entityPath, entityType)) .flatMap(channel -> channel.getSessionState(sessionId, getLinkName(sessionId))); } return result.onErrorMap((err) -> mapError(err, ServiceBusErrorSource.RECEIVE)); } /** * Map the error to {@link ServiceBusException} */ private Throwable mapError(Throwable throwable, ServiceBusErrorSource errorSource) { if (!(throwable instanceof ServiceBusException)) { return new ServiceBusException(throwable, errorSource); } return throwable; } boolean isConnectionClosed() { return this.connectionProcessor.isChannelClosed(); } boolean isManagementNodeLocksClosed() { return this.managementNodeLocks.isClosed(); } boolean isRenewalContainerClosed() { return this.renewalContainer.isClosed(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy