com.azure.messaging.servicebus.implementation.ManagementChannel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of azure-messaging-servicebus Show documentation
Show all versions of azure-messaging-servicebus Show documentation
Libraries built on Microsoft Azure Service Bus
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.messaging.servicebus.implementation;
import com.azure.core.amqp.exception.AmqpErrorCondition;
import com.azure.core.amqp.exception.AmqpErrorContext;
import com.azure.core.amqp.exception.AmqpException;
import com.azure.core.amqp.exception.AmqpResponseCode;
import com.azure.core.amqp.exception.SessionErrorContext;
import com.azure.core.amqp.implementation.ExceptionUtil;
import com.azure.core.amqp.implementation.MessageSerializer;
import com.azure.core.amqp.implementation.RequestResponseChannel;
import com.azure.core.amqp.implementation.RequestResponseUtils;
import com.azure.core.amqp.implementation.TokenManager;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.messaging.servicebus.ServiceBusErrorSource;
import com.azure.messaging.servicebus.ServiceBusException;
import com.azure.messaging.servicebus.ServiceBusMessage;
import com.azure.messaging.servicebus.ServiceBusReceivedMessage;
import com.azure.messaging.servicebus.ServiceBusTransactionContext;
import com.azure.messaging.servicebus.models.ServiceBusReceiveMode;
import org.apache.qpid.proton.Proton;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.UnsignedInteger;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.transaction.TransactionalState;
import org.apache.qpid.proton.amqp.transport.DeliveryState;
import org.apache.qpid.proton.message.Message;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SynchronousSink;
import java.nio.BufferOverflowException;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static com.azure.core.util.FluxUtil.fluxError;
import static com.azure.core.util.FluxUtil.monoError;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_GET_SESSION_STATE;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_PEEK;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_RENEW_SESSION_LOCK;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_SCHEDULE_MESSAGE;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_SET_SESSION_STATE;
import static com.azure.messaging.servicebus.implementation.ManagementConstants.OPERATION_UPDATE_DISPOSITION;
/**
* Channel responsible for Service Bus related metadata, peek and management plane operations. Management plane
* operations increasing quotas, etc.
*/
public class ManagementChannel implements ServiceBusManagementNode {
private final MessageSerializer messageSerializer;
private final TokenManager tokenManager;
private final Duration operationTimeout;
private final Mono createChannel;
private final String fullyQualifiedNamespace;
private final ClientLogger logger;
private final String entityPath;
private volatile boolean isDisposed;
ManagementChannel(Mono createChannel, String fullyQualifiedNamespace, String entityPath,
TokenManager tokenManager, MessageSerializer messageSerializer, Duration operationTimeout) {
this.createChannel = Objects.requireNonNull(createChannel, "'createChannel' cannot be null.");
this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace,
"'fullyQualifiedNamespace' cannot be null.");
this.logger = new ClientLogger(String.format("%s<%s>", ManagementChannel.class, entityPath));
this.entityPath = Objects.requireNonNull(entityPath, "'entityPath' cannot be null.");
this.messageSerializer = Objects.requireNonNull(messageSerializer, "'messageSerializer' cannot be null.");
this.tokenManager = Objects.requireNonNull(tokenManager, "'tokenManager' cannot be null.");
this.operationTimeout = Objects.requireNonNull(operationTimeout, "'operationTimeout' cannot be null.");
}
/**
* {@inheritDoc}
*/
@Override
public Mono cancelScheduledMessages(Iterable sequenceNumbers, String associatedLinkName) {
final List numbers = new ArrayList<>();
sequenceNumbers.forEach(s -> numbers.add(s));
if (numbers.isEmpty()) {
return Mono.empty();
}
return isAuthorized(ManagementConstants.OPERATION_CANCEL_SCHEDULED_MESSAGE)
.then(createChannel.flatMap(channel -> {
final Message requestMessage = createManagementMessage(
ManagementConstants.OPERATION_CANCEL_SCHEDULED_MESSAGE, associatedLinkName);
final Long[] longs = numbers.toArray(new Long[0]);
requestMessage.setBody(new AmqpValue(Collections.singletonMap(ManagementConstants.SEQUENCE_NUMBERS,
longs)));
return sendWithVerify(channel, requestMessage, null);
})).then();
}
/**
* {@inheritDoc}
*/
@Override
public Mono getSessionState(String sessionId, String associatedLinkName) {
if (sessionId == null) {
return monoError(logger, new NullPointerException("'sessionId' cannot be null."));
} else if (sessionId.isEmpty()) {
return monoError(logger, new IllegalArgumentException("'sessionId' cannot be blank."));
}
return isAuthorized(OPERATION_GET_SESSION_STATE).then(createChannel.flatMap(channel -> {
final Message message = createManagementMessage(OPERATION_GET_SESSION_STATE, associatedLinkName);
final Map body = new HashMap<>();
body.put(ManagementConstants.SESSION_ID, sessionId);
message.setBody(new AmqpValue(body));
return sendWithVerify(channel, message, null);
})).flatMap(response -> {
final Object value = ((AmqpValue) response.getBody()).getValue();
if (!(value instanceof Map)) {
return monoError(logger, Exceptions.propagate(new AmqpException(false, String.format(
"Body not expected when renewing session. Id: %s. Value: %s", sessionId, value),
getErrorContext())));
}
@SuppressWarnings("unchecked") final Map map = (Map) value;
final Object sessionState = map.get(ManagementConstants.SESSION_STATE);
if (sessionState == null) {
logger.info("sessionId[{}]. Does not have a session state.", sessionId);
return Mono.empty();
}
final byte[] state = ((Binary) sessionState).getArray();
return Mono.just(state);
});
}
/**
* {@inheritDoc}
*/
@Override
public Mono peek(long fromSequenceNumber, String sessionId, String associatedLinkName) {
return peek(fromSequenceNumber, sessionId, associatedLinkName, 1)
.next();
}
/**
* {@inheritDoc}
*/
@Override
public Flux peek(long fromSequenceNumber, String sessionId, String associatedLinkName,
int maxMessages) {
return isAuthorized(OPERATION_PEEK).thenMany(createChannel.flatMap(channel -> {
final Message message = createManagementMessage(OPERATION_PEEK, associatedLinkName);
// set mandatory properties on AMQP message body
final Map requestBody = new HashMap<>();
requestBody.put(ManagementConstants.FROM_SEQUENCE_NUMBER, fromSequenceNumber);
requestBody.put(ManagementConstants.MESSAGE_COUNT_KEY, maxMessages);
if (!CoreUtils.isNullOrEmpty(sessionId)) {
requestBody.put(ManagementConstants.SESSION_ID, sessionId);
}
message.setBody(new AmqpValue(requestBody));
return sendWithVerify(channel, message, null);
}).flatMapMany(response -> {
final List messages =
messageSerializer.deserializeList(response, ServiceBusReceivedMessage.class);
return Flux.fromIterable(messages);
}));
}
/**
* {@inheritDoc}
*/
@Override
public Flux receiveDeferredMessages(ServiceBusReceiveMode receiveMode, String sessionId,
String associatedLinkName, Iterable sequenceNumbers) {
if (sequenceNumbers == null) {
return fluxError(logger, new NullPointerException("'sequenceNumbers' cannot be null"));
}
final List numbers = new ArrayList<>();
sequenceNumbers.forEach(s -> numbers.add(s));
if (numbers.isEmpty()) {
return Flux.empty();
}
return isAuthorized(ManagementConstants.OPERATION_RECEIVE_BY_SEQUENCE_NUMBER)
.thenMany(createChannel.flatMap(channel -> {
final Message message = createManagementMessage(
ManagementConstants.OPERATION_RECEIVE_BY_SEQUENCE_NUMBER, associatedLinkName);
// set mandatory properties on AMQP message body
final Map requestBodyMap = new HashMap<>();
requestBodyMap.put(ManagementConstants.SEQUENCE_NUMBERS, numbers.toArray(new Long[0]));
requestBodyMap.put(ManagementConstants.RECEIVER_SETTLE_MODE,
UnsignedInteger.valueOf(receiveMode == ServiceBusReceiveMode.RECEIVE_AND_DELETE ? 0 : 1));
if (!CoreUtils.isNullOrEmpty(sessionId)) {
requestBodyMap.put(ManagementConstants.SESSION_ID, sessionId);
}
message.setBody(new AmqpValue(requestBodyMap));
return sendWithVerify(channel, message, null);
}).flatMapMany(amqpMessage -> {
final List messageList =
messageSerializer.deserializeList(amqpMessage, ServiceBusReceivedMessage.class);
return Flux.fromIterable(messageList);
}));
}
private Throwable mapError(Throwable throwable) {
if (throwable instanceof AmqpException) {
return new ServiceBusException(throwable, ServiceBusErrorSource.MANAGEMENT);
}
return throwable;
}
/**
* {@inheritDoc}
*/
@Override
public Mono renewMessageLock(String lockToken, String associatedLinkName) {
return isAuthorized(OPERATION_PEEK).then(createChannel.flatMap(channel -> {
final Message requestMessage = createManagementMessage(ManagementConstants.OPERATION_RENEW_LOCK,
associatedLinkName);
final Map requestBody = new HashMap<>();
requestBody.put(ManagementConstants.LOCK_TOKENS_KEY, new UUID[]{UUID.fromString(lockToken)});
requestMessage.setBody(new AmqpValue(requestBody));
return sendWithVerify(channel, requestMessage, null);
}).map(responseMessage -> {
final List renewTimeList = messageSerializer.deserializeList(responseMessage,
OffsetDateTime.class);
if (CoreUtils.isNullOrEmpty(renewTimeList)) {
throw logger.logExceptionAsError(Exceptions.propagate(new AmqpException(false, String.format(
"Service bus response empty. Could not renew message with lock token: '%s'.", lockToken),
getErrorContext())));
}
return renewTimeList.get(0);
}));
}
@Override
public Mono renewSessionLock(String sessionId, String associatedLinkName) {
if (sessionId == null) {
return monoError(logger, new NullPointerException("'sessionId' cannot be null."));
} else if (sessionId.isEmpty()) {
return monoError(logger, new IllegalArgumentException("'sessionId' cannot be blank."));
}
return isAuthorized(OPERATION_RENEW_SESSION_LOCK).then(createChannel.flatMap(channel -> {
final Message message = createManagementMessage(OPERATION_RENEW_SESSION_LOCK, associatedLinkName);
final Map body = new HashMap<>();
body.put(ManagementConstants.SESSION_ID, sessionId);
message.setBody(new AmqpValue(body));
return sendWithVerify(channel, message, null);
})).map(response -> {
final Object value = ((AmqpValue) response.getBody()).getValue();
if (!(value instanceof Map)) {
throw logger.logExceptionAsError(Exceptions.propagate(new AmqpException(false, String.format(
"Body not expected when renewing session. Id: %s. Value: %s", sessionId, value),
getErrorContext())));
}
@SuppressWarnings("unchecked") final Map map = (Map) value;
final Object expirationValue = map.get(ManagementConstants.EXPIRATION);
if (!(expirationValue instanceof Date)) {
throw logger.logExceptionAsError(Exceptions.propagate(new AmqpException(false, String.format(
"Expiration is not of type Date when renewing session. Id: %s. Value: %s", sessionId,
expirationValue), getErrorContext())));
}
return ((Date) expirationValue).toInstant().atOffset(ZoneOffset.UTC);
});
}
/**
* {@inheritDoc}
*/
@Override
public Flux schedule(List messages, OffsetDateTime scheduledEnqueueTime,
int maxLinkSize, String associatedLinkName, ServiceBusTransactionContext transactionContext) {
return isAuthorized(OPERATION_SCHEDULE_MESSAGE).thenMany(createChannel.flatMap(channel -> {
final Collection