com.azure.messaging.eventhubs.EventHubClientBuilder Maven / Gradle / Ivy
Show all versions of azure-messaging-eventhubs Show documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.messaging.eventhubs;
import com.azure.core.amqp.AmqpClientOptions;
import com.azure.core.amqp.AmqpRetryOptions;
import com.azure.core.amqp.AmqpTransportType;
import com.azure.core.amqp.ProxyOptions;
import com.azure.core.amqp.client.traits.AmqpTrait;
import com.azure.core.amqp.implementation.AzureTokenManagerProvider;
import com.azure.core.amqp.implementation.ConnectionOptions;
import com.azure.core.amqp.implementation.ConnectionStringProperties;
import com.azure.core.amqp.implementation.MessageSerializer;
import com.azure.core.amqp.implementation.ReactorHandlerProvider;
import com.azure.core.amqp.implementation.ReactorProvider;
import com.azure.core.amqp.implementation.StringUtil;
import com.azure.core.amqp.implementation.TokenManagerProvider;
import com.azure.core.amqp.models.CbsAuthorizationType;
import com.azure.core.annotation.ServiceClientBuilder;
import com.azure.core.annotation.ServiceClientProtocol;
import com.azure.core.client.traits.AzureNamedKeyCredentialTrait;
import com.azure.core.client.traits.AzureSasCredentialTrait;
import com.azure.core.client.traits.ConfigurationTrait;
import com.azure.core.client.traits.ConnectionStringTrait;
import com.azure.core.client.traits.TokenCredentialTrait;
import com.azure.core.credential.AzureNamedKeyCredential;
import com.azure.core.credential.AzureSasCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.core.exception.AzureException;
import com.azure.core.util.ClientOptions;
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.metrics.Meter;
import com.azure.core.util.metrics.MeterProvider;
import com.azure.messaging.eventhubs.implementation.ClientConstants;
import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection;
import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor;
import com.azure.messaging.eventhubs.implementation.EventHubReactorAmqpConnection;
import com.azure.messaging.eventhubs.implementation.EventHubSharedKeyCredential;
import com.azure.messaging.eventhubs.implementation.instrumentation.EventHubsTracer;
import org.apache.qpid.proton.engine.SslDomain;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static com.azure.messaging.eventhubs.implementation.ClientConstants.CONNECTION_ID_KEY;
/**
* This class provides a fluent builder API to aid the instantiation of {@link EventHubProducerAsyncClient}, {@link
* EventHubProducerClient}, {@link EventHubConsumerAsyncClient}, and {@link EventHubConsumerClient}. Calling any of the
* {@code .build*Client()} methods will create an instance of the respective client.
*
*
* Credentials are required to perform operations against Azure Event Hubs. They can be set by using
* one of the following methods:
*
* - {@link #connectionString(String) connectionString(String)} with a connection string to a specific Event Hub.
*
* - {@link #connectionString(String, String) connectionString(String, String)} with an Event Hub namespace
* connection string and the Event Hub name.
* - {@link #credential(String, String, TokenCredential) credential(String, String, TokenCredential)} with the
* fully qualified namespace, Event Hub name, and a set of credentials authorized to use the Event Hub.
*
*
*
*
* In addition, the consumer group is required when creating {@link EventHubConsumerAsyncClient} or
* {@link EventHubConsumerClient}.
*
*
* Creating an asynchronous {@link EventHubProducerAsyncClient} using Event Hubs namespace connection string
*
* In the sample, the namespace connection string is used to create an asynchronous Event Hub producer. Notice that
* {@code "EntityPath"} is not a component in the connection string.
*
*
*
* // The required parameter is a way to authenticate with Event Hubs using credentials.
* // The connectionString provides a way to authenticate with Event Hub.
* EventHubProducerAsyncClient producer = new EventHubClientBuilder()
* .connectionString(
* "Endpoint={fully-qualified-namespace};SharedAccessKeyName={policy-name};SharedAccessKey={key}",
* "event-hub-name")
* .buildAsyncProducerClient();
*
*
*
* Creating a synchronous {@link EventHubConsumerClient} using an Event Hub instance connection string
*
* In the sample, the namespace connection string is used to create a synchronous Event Hub consumer. Notice that
* {@code "EntityPath"} is in the connection string.
*
*
*
* // The required parameters are `consumerGroup`, and a way to authenticate with Event Hubs using credentials.
* EventHubConsumerClient consumer = new EventHubClientBuilder()
* .connectionString("Endpoint={fully-qualified-namespace};SharedAccessKeyName={policy-name};"
* + "SharedAccessKey={key};Entity-Path={hub-name}")
* .consumerGroup("$DEFAULT")
* .buildConsumerClient();
*
*
*
* Creating producers and consumers that share the same connection
* By default, a dedicated connection is created for each producer and consumer created from the builder. If users
* wish to use the same underlying connection, they can toggle {@link #shareConnection() shareConnection()}.
*
*
*
* // Toggling `shareConnection` instructs the builder to use the same underlying connection
* // for each consumer or producer created using the same builder instance.
* EventHubClientBuilder builder = new EventHubClientBuilder()
* .connectionString("event-hubs-instance-connection-string")
* .shareConnection();
*
* // Both the producer and consumer created share the same underlying connection.
* EventHubProducerAsyncClient producer = builder.buildAsyncProducerClient();
* EventHubConsumerAsyncClient consumer = builder
* .consumerGroup("my-consumer-group")
* .buildAsyncConsumerClient();
*
*
*
* @see EventHubProducerAsyncClient
* @see EventHubProducerClient
* @see EventHubConsumerAsyncClient
* @see EventHubConsumerClient
*/
@ServiceClientBuilder(serviceClients = {EventHubProducerAsyncClient.class, EventHubProducerClient.class,
EventHubConsumerAsyncClient.class, EventHubConsumerClient.class}, protocol = ServiceClientProtocol.AMQP)
public class EventHubClientBuilder implements
TokenCredentialTrait,
AzureNamedKeyCredentialTrait,
ConnectionStringTrait,
AzureSasCredentialTrait,
AmqpTrait,
ConfigurationTrait {
// Default number of events to fetch when creating the consumer.
static final int DEFAULT_PREFETCH_COUNT = 500;
// Default number of events to fetch for a sync client. The sync client operates in "pull" mode.
// So, limit the prefetch to just 1 by default.
static final int DEFAULT_PREFETCH_COUNT_FOR_SYNC_CLIENT = 1;
static final AmqpRetryOptions DEFAULT_RETRY = new AmqpRetryOptions()
.setTryTimeout(ClientConstants.OPERATION_TIMEOUT);
/**
* The name of the default consumer group in the Event Hubs service.
*/
public static final String DEFAULT_CONSUMER_GROUP_NAME = "$Default";
/**
* The minimum value allowed for the prefetch count of the consumer.
*/
private static final int MINIMUM_PREFETCH_COUNT = 1;
/**
* The maximum value allowed for the prefetch count of the consumer.
*/
private static final int MAXIMUM_PREFETCH_COUNT = 8000;
private static final String EVENTHUBS_PROPERTIES_FILE = "azure-messaging-eventhubs.properties";
private static final String NAME_KEY = "name";
private static final String VERSION_KEY = "version";
private static final String LIBRARY_NAME;
private static final String LIBRARY_VERSION;
private static final String UNKNOWN = "UNKNOWN";
private static final String AZURE_EVENT_HUBS_CONNECTION_STRING = "AZURE_EVENT_HUBS_CONNECTION_STRING";
private static final Pattern HOST_PORT_PATTERN = Pattern.compile("^[^:]+:\\d+");
private static final ClientLogger LOGGER = new ClientLogger(EventHubClientBuilder.class);
private final Object connectionLock = new Object();
private final AtomicBoolean isSharedConnection = new AtomicBoolean();
private TokenCredential credentials;
private Configuration configuration;
private ProxyOptions proxyOptions;
private AmqpRetryOptions retryOptions;
private Scheduler scheduler;
private AmqpTransportType transport;
private String fullyQualifiedNamespace;
private String eventHubName;
private String consumerGroup;
private EventHubConnectionProcessor eventHubConnectionProcessor;
private Integer prefetchCount;
private ClientOptions clientOptions;
private SslDomain.VerifyMode verifyMode;
private URL customEndpointAddress;
static {
final Map properties = CoreUtils.getProperties(EVENTHUBS_PROPERTIES_FILE);
LIBRARY_NAME = properties.getOrDefault(NAME_KEY, UNKNOWN);
LIBRARY_VERSION = properties.getOrDefault(VERSION_KEY, UNKNOWN);
}
/**
* Keeps track of the open clients that were created from this builder when there is a shared connection.
*/
private final AtomicInteger openClients = new AtomicInteger();
/**
* Creates a new instance with the default transport {@link AmqpTransportType#AMQP} and a non-shared connection. A
* non-shared connection means that a dedicated AMQP connection is created for every Event Hub consumer or producer
* created using the builder.
*/
public EventHubClientBuilder() {
transport = AmqpTransportType.AMQP;
}
/**
* Sets the credential information given a connection string to the Event Hub instance or the Event Hubs namespace.
*
*
* If the connection string is copied from the Event Hubs namespace, it will likely not contain the name to the
* desired Event Hub, which is needed. In this case, the name can be added manually by adding {@literal
* "EntityPath=EVENT_HUB_NAME"} to the end of the connection string. For example, "EntityPath=telemetry-hub".
*
*
*
* If you have defined a shared access policy directly on the Event Hub itself, then copying the connection string
* from that Event Hub will result in a connection string that contains the name.
*
*
* @param connectionString The connection string to use for connecting to the Event Hub instance or Event Hubs
* instance. It is expected that the Event Hub name and the shared access key properties are contained in this
* connection string.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code connectionString} is null or empty. If {@code fullyQualifiedNamespace}
* in the connection string is null.
* @throws NullPointerException if a credential could not be extracted
* @throws AzureException If the shared access signature token credential could not be created using the
* connection string.
*/
@Override
public EventHubClientBuilder connectionString(String connectionString) {
final ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
this.fullyQualifiedNamespace = Objects.requireNonNull(properties.getEndpoint().getHost(),
"'fullyQualifiedNamespace' cannot be null.");
this.credentials = getTokenCredential(properties);
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'host' cannot be an empty string."));
}
if (!CoreUtils.isNullOrEmpty(properties.getEntityPath())) {
this.eventHubName = properties.getEntityPath();
}
return this;
}
private TokenCredential getTokenCredential(ConnectionStringProperties properties) {
TokenCredential tokenCredential;
if (properties.getSharedAccessSignature() == null) {
tokenCredential = new EventHubSharedKeyCredential(properties.getSharedAccessKeyName(),
properties.getSharedAccessKey(), ClientConstants.TOKEN_VALIDITY);
} else {
tokenCredential = new EventHubSharedKeyCredential(properties.getSharedAccessSignature());
}
return tokenCredential;
}
/**
* Sets the client options.
*
* @param clientOptions The client options.
* @return The updated {@link EventHubClientBuilder} object.
*/
@Override
public EventHubClientBuilder clientOptions(ClientOptions clientOptions) {
this.clientOptions = clientOptions;
return this;
}
/**
* Sets the credential information given a connection string to the Event Hubs namespace and name to a specific
* Event Hub instance.
*
* @param connectionString The connection string to use for connecting to the Event Hubs namespace; it is
* expected that the shared access key properties are contained in this connection string, but not the Event Hub
* name.
* @param eventHubName The name of the Event Hub to connect the client to.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws NullPointerException if {@code connectionString} or {@code eventHubName} is null.
* @throws IllegalArgumentException if {@code connectionString} or {@code eventHubName} is an empty string. Or,
* if the {@code connectionString} contains the Event Hub name.
* @throws AzureException If the shared access signature token credential could not be created using the
* connection string.
*/
public EventHubClientBuilder connectionString(String connectionString, String eventHubName) {
Objects.requireNonNull(connectionString, "'connectionString' cannot be null.");
Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null.");
if (connectionString.isEmpty()) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
"'connectionString' cannot be an empty string."));
} else if (eventHubName.isEmpty()) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
final ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
TokenCredential tokenCredential = getTokenCredential(properties);
if (!CoreUtils.isNullOrEmpty(properties.getEntityPath())
&& !eventHubName.equals(properties.getEntityPath())) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(String.format(Locale.US,
"'connectionString' contains an Event Hub name [%s] and it does not match the given "
+ "'eventHubName' parameter [%s]. Please use the credentials(String connectionString) overload. "
+ "Or supply a 'connectionString' without 'EntityPath' in it.",
properties.getEntityPath(), eventHubName)));
}
return credential(properties.getEndpoint().getHost(), eventHubName, tokenCredential);
}
/**
* Sets the configuration store that is used during construction of the service client.
*
* If not specified, the default configuration store is used to configure the {@link EventHubAsyncClient}. Use
* {@link Configuration#NONE} to bypass using configuration settings during construction.
*
* @param configuration The configuration store used to configure the {@link EventHubAsyncClient}.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
@Override
public EventHubClientBuilder configuration(Configuration configuration) {
this.configuration = configuration;
return this;
}
/**
* Sets a custom endpoint address when connecting to the Event Hubs service. This can be useful when your network
* does not allow connecting to the standard Azure Event Hubs endpoint address, but does allow connecting through
* an intermediary. For example: {@literal https://my.custom.endpoint.com:55300}.
*
* If no port is specified, the default port for the {@link #transportType(AmqpTransportType) transport type} is
* used.
*
* @param customEndpointAddress The custom endpoint address.
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code customEndpointAddress} cannot be parsed into a valid {@link URL}.
*/
public EventHubClientBuilder customEndpointAddress(String customEndpointAddress) {
if (customEndpointAddress == null) {
this.customEndpointAddress = null;
return this;
}
try {
this.customEndpointAddress = new URL(customEndpointAddress);
} catch (MalformedURLException e) {
throw LOGGER.logExceptionAsError(
new IllegalArgumentException(customEndpointAddress + " : is not a valid URL.", e));
}
return this;
}
/**
* Sets the fully qualified name for the Event Hubs namespace.
*
* @param fullyQualifiedNamespace The fully qualified name for the Event Hubs namespace. This is likely to be
* similar to {@literal "{your-namespace}.servicebus.windows.net}".
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code fullyQualifiedNamespace} is an empty string.
* @throws NullPointerException if {@code fullyQualifiedNamespace} is null.
*/
public EventHubClientBuilder fullyQualifiedNamespace(String fullyQualifiedNamespace) {
this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace,
"'fullyQualifiedNamespace' cannot be null.");
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("'fullyQualifiedNamespace' cannot be an empty string."));
}
return this;
}
private String getAndValidateFullyQualifiedNamespace() {
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("'fullyQualifiedNamespace' cannot be an empty string."));
}
return fullyQualifiedNamespace;
}
/**
* Sets the name of the Event Hub to connect the client to.
*
* @param eventHubName The name of the Event Hub to connect the client to.
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code eventHubName} is an empty string.
* @throws NullPointerException if {@code eventHubName} is null.
*/
public EventHubClientBuilder eventHubName(String eventHubName) {
this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null.");
if (CoreUtils.isNullOrEmpty(eventHubName)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
return this;
}
/**
* Toggles the builder to use the same connection for producers or consumers that are built from this instance. By
* default, a new connection is constructed and used created for each Event Hub consumer or producer created.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
public EventHubClientBuilder shareConnection() {
this.isSharedConnection.set(true);
return this;
}
/**
* Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
*
* @param fullyQualifiedNamespace The fully qualified name for the Event Hubs namespace. This is likely to be
* similar to {@literal "{your-namespace}.servicebus.windows.net}".
* @param eventHubName The name of the Event Hub to connect the client to.
* @param credential The token credential to use for authorization. Access controls may be specified by the
* Event Hubs namespace or the requested Event Hub, depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code fullyQualifiedNamespace} or {@code eventHubName} is an empty
* string.
* @throws NullPointerException if {@code fullyQualifiedNamespace}, {@code eventHubName}, {@code credentials} is
* null.
*/
public EventHubClientBuilder credential(String fullyQualifiedNamespace, String eventHubName,
TokenCredential credential) {
this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace,
"'fullyQualifiedNamespace' cannot be null.");
this.credentials = Objects.requireNonNull(credential, "'credential' cannot be null.");
this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null.");
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'host' cannot be an empty string."));
} else if (CoreUtils.isNullOrEmpty(eventHubName)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
return this;
}
/**
* Sets the {@link TokenCredential} used to authorize requests sent to the service. Refer to the Azure SDK for Java
* identity and authentication
* documentation for more details on proper usage of the {@link TokenCredential} type.
*
* @param credential The token credential to use for authorization. Access controls may be specified by the
* Event Hubs namespace or the requested Event Hub, depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws NullPointerException if {@code credentials} is null.
*/
@Override
public EventHubClientBuilder credential(TokenCredential credential) {
this.credentials = Objects.requireNonNull(credential, "'credential' cannot be null.");
return this;
}
/**
* Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
*
* @param fullyQualifiedNamespace The fully qualified name for the Event Hubs namespace. This is likely to be
* similar to {@literal "{your-namespace}.servicebus.windows.net}".
* @param eventHubName The name of the Event Hub to connect the client to.
* @param credential The shared access name and key credential to use for authorization.
* Access controls may be specified by the Event Hubs namespace or the requested Event Hub,
* depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code fullyQualifiedNamespace} or {@code eventHubName} is an empty
* string.
* @throws NullPointerException if {@code fullyQualifiedNamespace}, {@code eventHubName}, {@code credentials} is
* null.
*/
public EventHubClientBuilder credential(String fullyQualifiedNamespace, String eventHubName,
AzureNamedKeyCredential credential) {
this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace,
"'fullyQualifiedNamespace' cannot be null.");
this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null.");
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'host' cannot be an empty string."));
} else if (CoreUtils.isNullOrEmpty(eventHubName)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
Objects.requireNonNull(credential, "'credential' cannot be null.");
this.credentials = new EventHubSharedKeyCredential(credential.getAzureNamedKey().getName(),
credential.getAzureNamedKey().getKey(), ClientConstants.TOKEN_VALIDITY);
return this;
}
/**
* Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
*
* @param credential The shared access name and key credential to use for authorization.
* Access controls may be specified by the Event Hubs namespace or the requested Event Hub,
* depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws NullPointerException if {@code credentials} is null.
*/
@Override
public EventHubClientBuilder credential(AzureNamedKeyCredential credential) {
Objects.requireNonNull(credential, "'credential' cannot be null.");
this.credentials = new EventHubSharedKeyCredential(credential.getAzureNamedKey().getName(),
credential.getAzureNamedKey().getKey(), ClientConstants.TOKEN_VALIDITY);
return this;
}
/**
* Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
*
* @param fullyQualifiedNamespace The fully qualified name for the Event Hubs namespace. This is likely to be
* similar to {@literal "{your-namespace}.servicebus.windows.net}".
* @param eventHubName The name of the Event Hub to connect the client to.
* @param credential The shared access signature credential to use for authorization.
* Access controls may be specified by the Event Hubs namespace or the requested Event Hub,
* depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code fullyQualifiedNamespace} or {@code eventHubName} is an empty
* string.
* @throws NullPointerException if {@code fullyQualifiedNamespace}, {@code eventHubName}, {@code credentials} is
* null.
*/
public EventHubClientBuilder credential(String fullyQualifiedNamespace, String eventHubName,
AzureSasCredential credential) {
this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace,
"'fullyQualifiedNamespace' cannot be null.");
this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null.");
if (CoreUtils.isNullOrEmpty(fullyQualifiedNamespace)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'host' cannot be an empty string."));
} else if (CoreUtils.isNullOrEmpty(eventHubName)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
Objects.requireNonNull(credential, "'credential' cannot be null.");
this.credentials = new EventHubSharedKeyCredential(credential.getSignature());
return this;
}
/**
* Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
*
* @param credential The shared access signature credential to use for authorization.
* Access controls may be specified by the Event Hubs namespace or the requested Event Hub,
* depending on Azure configuration.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws NullPointerException if {@code credentials} is null.
*/
@Override
public EventHubClientBuilder credential(AzureSasCredential credential) {
Objects.requireNonNull(credential, "'credential' cannot be null.");
this.credentials = new EventHubSharedKeyCredential(credential.getSignature());
return this;
}
/**
* Sets the proxy configuration to use for {@link EventHubAsyncClient}. When a proxy is configured, {@link
* AmqpTransportType#AMQP_WEB_SOCKETS} must be used for the transport type.
*
* @param proxyOptions The proxy configuration to use.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
@Override
public EventHubClientBuilder proxyOptions(ProxyOptions proxyOptions) {
this.proxyOptions = proxyOptions;
return this;
}
/**
* Sets the transport type by which all the communication with Azure Event Hubs occurs. Default value is {@link
* AmqpTransportType#AMQP}.
*
* @param transport The transport type to use.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
@Override
public EventHubClientBuilder transportType(AmqpTransportType transport) {
this.transport = transport;
return this;
}
/**
* Sets the retry policy for {@link EventHubAsyncClient}. If not specified, the default retry options are used.
*
* @param retryOptions The retry policy to use.
*
* @return The updated {@link EventHubClientBuilder} object.
* @deprecated Replaced by {@link #retryOptions(AmqpRetryOptions)}.
*/
@Deprecated
public EventHubClientBuilder retry(AmqpRetryOptions retryOptions) {
this.retryOptions = retryOptions;
return this;
}
/**
* Sets the retry policy for {@link EventHubAsyncClient}. If not specified, the default retry options are used.
*
* @param retryOptions The retry policy to use.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
@Override
public EventHubClientBuilder retryOptions(AmqpRetryOptions retryOptions) {
this.retryOptions = retryOptions;
return this;
}
/**
* Sets the name of the consumer group this consumer is associated with. Events are read in the context of this
* group. The name of the consumer group that is created by default is {@link #DEFAULT_CONSUMER_GROUP_NAME
* "$Default"}.
*
* @param consumerGroup The name of the consumer group this consumer is associated with. Events are read in the
* context of this group. The name of the consumer group that is created by default is {@link
* #DEFAULT_CONSUMER_GROUP_NAME "$Default"}.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
public EventHubClientBuilder consumerGroup(String consumerGroup) {
this.consumerGroup = consumerGroup;
return this;
}
/**
* Sets the count used by the receiver to control the number of events the Event Hub consumer will actively receive
* and queue locally without regard to whether a receive operation is currently active.
*
* @param prefetchCount The amount of events to queue locally.
*
* @return The updated {@link EventHubClientBuilder} object.
* @throws IllegalArgumentException if {@code prefetchCount} is less than {@link #MINIMUM_PREFETCH_COUNT 1} or
* greater than {@link #MAXIMUM_PREFETCH_COUNT 8000}.
*/
public EventHubClientBuilder prefetchCount(int prefetchCount) {
if (prefetchCount < MINIMUM_PREFETCH_COUNT) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(String.format(Locale.US,
"PrefetchCount, '%s' has to be above %s", prefetchCount, MINIMUM_PREFETCH_COUNT)));
}
if (prefetchCount > MAXIMUM_PREFETCH_COUNT) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(String.format(Locale.US,
"PrefetchCount, '%s', has to be below %s", prefetchCount, MAXIMUM_PREFETCH_COUNT)));
}
this.prefetchCount = prefetchCount;
return this;
}
/**
* Package-private method that gets the prefetch count.
*
* @return Gets the prefetch count or {@code null} if it has not been set.
* @see #DEFAULT_PREFETCH_COUNT for default prefetch count.
*/
Integer getPrefetchCount() {
return prefetchCount;
}
/**
* Package-private method that sets the scheduler for the created Event Hub client.
*
* @param scheduler Scheduler to set.
*
* @return The updated {@link EventHubClientBuilder} object.
*/
EventHubClientBuilder scheduler(Scheduler scheduler) {
this.scheduler = scheduler;
return this;
}
/**
* Package-private method that sets the verify mode for this connection.
*
* @param verifyMode The verification mode.
* @return The updated {@link EventHubClientBuilder} object.
*/
EventHubClientBuilder verifyMode(SslDomain.VerifyMode verifyMode) {
this.verifyMode = verifyMode;
return this;
}
/**
* Creates a new {@link EventHubConsumerAsyncClient} based on the options set on this builder. Every time {@code
* buildAsyncConsumer()} is invoked, a new instance of {@link EventHubConsumerAsyncClient} is created.
*
* @return A new {@link EventHubConsumerAsyncClient} with the configured options.
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Also, if
* {@link #consumerGroup(String)} have not been set. And if a proxy is specified but the transport type is not
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the {@code eventHubName} has not been set.
*/
public EventHubConsumerAsyncClient buildAsyncConsumerClient() {
if (CoreUtils.isNullOrEmpty(consumerGroup)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'consumerGroup' cannot be null or an empty "
+ "string. using EventHubClientBuilder.consumerGroup(String)"));
}
return buildAsyncClient().createConsumer(consumerGroup, prefetchCount, false);
}
/**
* Creates a new {@link EventHubConsumerClient} based on the options set on this builder. Every time {@code
* buildConsumer()} is invoked, a new instance of {@link EventHubConsumerClient} is created.
*
* @return A new {@link EventHubConsumerClient} with the configured options.
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Also, if
* {@link #consumerGroup(String)} have not been set. And if a proxy is specified but the transport type is not
* {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the {@code eventHubName} has not been set.
*/
public EventHubConsumerClient buildConsumerClient() {
return buildClient().createConsumer(consumerGroup, prefetchCount);
}
/**
* Creates a new {@link EventHubProducerAsyncClient} based on options set on this builder. Every time {@code
* buildAsyncProducer()} is invoked, a new instance of {@link EventHubProducerAsyncClient} is created.
*
* @return A new {@link EventHubProducerAsyncClient} instance with all the configured options.
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a
* proxy is specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
* Or, if the {@code eventHubName} has not been set.
*/
public EventHubProducerAsyncClient buildAsyncProducerClient() {
return buildAsyncClient().createProducer();
}
/**
* Creates a new {@link EventHubProducerClient} based on options set on this builder. Every time {@code
* buildAsyncProducer()} is invoked, a new instance of {@link EventHubProducerClient} is created.
*
* @return A new {@link EventHubProducerClient} instance with all the configured options.
* @throws IllegalArgumentException If shared connection is not used and the credentials have not been set using
* either {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a
* proxy is specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
* Or, if the {@code eventHubName} has not been set.
*/
public EventHubProducerClient buildProducerClient() {
return buildClient().createProducer();
}
/**
* Creates a new {@link EventHubAsyncClient} based on options set on this builder. Every time {@code
* buildAsyncClient()} is invoked, a new instance of {@link EventHubAsyncClient} is created.
*
*
* The following options are used if ones are not specified in the builder:
*
*
* - If no configuration is specified, the {@link Configuration#getGlobalConfiguration() global configuration}
* is used to provide any shared configuration values. The configuration values read are the {@link
* Configuration#PROPERTY_HTTP_PROXY}, {@link ProxyOptions#PROXY_USERNAME}, and {@link
* ProxyOptions#PROXY_PASSWORD}.
* - If no retry is specified, the default retry options are used.
* - If no proxy is specified, the builder checks the {@link Configuration#getGlobalConfiguration() global
* configuration} for a configured proxy, then it checks to see if a system proxy is configured.
* - If no timeout is specified, a {@link ClientConstants#OPERATION_TIMEOUT timeout of one minute} is used.
*
*
* @return A new {@link EventHubAsyncClient} instance with all the configured options.
* @throws IllegalArgumentException if the credentials have not been set using either {@link
* #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a proxy is
* specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}. Or, if the
* {@code eventHubName} has not been set.
*/
EventHubAsyncClient buildAsyncClient() {
if (retryOptions == null) {
retryOptions = DEFAULT_RETRY;
}
if (scheduler == null) {
scheduler = Schedulers.boundedElastic();
}
if (prefetchCount == null) {
prefetchCount = DEFAULT_PREFETCH_COUNT;
}
final Meter meter = MeterProvider.getDefaultProvider().createMeter(LIBRARY_NAME, LIBRARY_VERSION,
clientOptions == null ? null : clientOptions.getMetricsOptions());
final MessageSerializer messageSerializer = new EventHubMessageSerializer();
final EventHubConnectionProcessor processor;
if (isSharedConnection.get()) {
synchronized (connectionLock) {
if (eventHubConnectionProcessor == null) {
eventHubConnectionProcessor = buildConnectionProcessor(messageSerializer, meter);
}
}
processor = eventHubConnectionProcessor;
final int numberOfOpenClients = openClients.incrementAndGet();
LOGGER.info("# of open clients with shared connection: {}", numberOfOpenClients);
} else {
processor = buildConnectionProcessor(messageSerializer, meter);
}
String identifier;
if (clientOptions instanceof AmqpClientOptions) {
String clientOptionIdentifier = ((AmqpClientOptions) clientOptions).getIdentifier();
identifier = CoreUtils.isNullOrEmpty(clientOptionIdentifier) ? UUID.randomUUID().toString() : clientOptionIdentifier;
} else {
identifier = UUID.randomUUID().toString();
}
return new EventHubAsyncClient(processor, messageSerializer, scheduler,
isSharedConnection.get(), this::onClientClose, identifier, meter, EventHubsTracer.getDefaultTracer());
}
/**
* Creates a new {@link EventHubClient} based on options set on this builder. Every time {@code buildClient()} is
* invoked, a new instance of {@link EventHubClient} is created.
*
*
* The following options are used if ones are not specified in the builder:
*
*
* - If no configuration is specified, the {@link Configuration#getGlobalConfiguration() global configuration}
* is used to provide any shared configuration values. The configuration values read are the {@link
* Configuration#PROPERTY_HTTP_PROXY}, {@link ProxyOptions#PROXY_USERNAME}, and {@link
* ProxyOptions#PROXY_PASSWORD}.
* - If no retry is specified, the default retry options are used.
* - If no proxy is specified, the builder checks the {@link Configuration#getGlobalConfiguration() global
* configuration} for a configured proxy, then it checks to see if a system proxy is configured.
* - If no timeout is specified, a {@link ClientConstants#OPERATION_TIMEOUT timeout of one minute} is used.
* - If no scheduler is specified, an {@link Schedulers#boundedElastic() elastic scheduler} is used.
*
*
* @return A new {@link EventHubClient} instance with all the configured options.
* @throws IllegalArgumentException if the credentials have not been set using either {@link
* #connectionString(String)} or {@link #credential(String, String, TokenCredential)}. Or, if a proxy is
* specified but the transport type is not {@link AmqpTransportType#AMQP_WEB_SOCKETS web sockets}.
*/
EventHubClient buildClient() {
if (prefetchCount == null) {
// For sync clients, do not prefetch eagerly as the client can "pull" as many events as required.
prefetchCount = DEFAULT_PREFETCH_COUNT_FOR_SYNC_CLIENT;
}
final EventHubAsyncClient client = buildAsyncClient();
return new EventHubClient(client, retryOptions);
}
void onClientClose() {
synchronized (connectionLock) {
final int numberOfOpenClients = openClients.decrementAndGet();
LOGGER.info("Closing a dependent client. # of open clients: {}", numberOfOpenClients);
if (numberOfOpenClients > 0) {
return;
}
if (numberOfOpenClients < 0) {
LOGGER.warning("There should not be less than 0 clients. actual: {}", numberOfOpenClients);
}
LOGGER.info("No more open clients, closing shared connection.");
if (eventHubConnectionProcessor != null) {
eventHubConnectionProcessor.dispose();
eventHubConnectionProcessor = null;
} else {
LOGGER.warning("Shared EventHubConnectionProcessor was already disposed.");
}
}
}
Meter createMeter() {
return MeterProvider.getDefaultProvider().createMeter(LIBRARY_NAME, LIBRARY_VERSION,
clientOptions == null ? null : clientOptions.getMetricsOptions());
}
private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer messageSerializer, Meter meter) {
final ConnectionOptions connectionOptions = getConnectionOptions();
final Supplier getEventHubName = () -> {
if (CoreUtils.isNullOrEmpty(eventHubName)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'eventHubName' cannot be an empty string."));
}
return eventHubName;
};
final Flux connectionFlux = Flux.create(sink -> {
sink.onRequest(request -> {
if (request == 0) {
return;
} else if (request > 1) {
sink.error(LOGGER.logExceptionAsWarning(new IllegalArgumentException(
"Requested more than one connection. Only emitting one. Request: " + request)));
return;
}
final String connectionId = StringUtil.getRandomString("MF");
LOGGER.atInfo()
.addKeyValue(CONNECTION_ID_KEY, connectionId)
.log("Emitting a single connection.");
final TokenManagerProvider tokenManagerProvider = new AzureTokenManagerProvider(
connectionOptions.getAuthorizationType(), connectionOptions.getFullyQualifiedNamespace(),
connectionOptions.getAuthorizationScope());
final ReactorProvider provider = new ReactorProvider();
final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider, meter);
final EventHubAmqpConnection connection = new EventHubReactorAmqpConnection(connectionId,
connectionOptions, getEventHubName.get(), provider, handlerProvider, tokenManagerProvider,
messageSerializer);
sink.next(connection);
});
});
return connectionFlux.subscribeWith(new EventHubConnectionProcessor(
connectionOptions.getFullyQualifiedNamespace(), getEventHubName.get(), connectionOptions.getRetry()));
}
private ConnectionOptions getConnectionOptions() {
Configuration buildConfiguration = configuration == null
? Configuration.getGlobalConfiguration().clone()
: configuration;
if (credentials == null) {
final String connectionString = buildConfiguration.get(AZURE_EVENT_HUBS_CONNECTION_STRING);
if (CoreUtils.isNullOrEmpty(connectionString)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Credentials have not been set. "
+ "They can be set using: connectionString(String), connectionString(String, String), "
+ "credentials(String, String, TokenCredential), or setting the environment variable '"
+ AZURE_EVENT_HUBS_CONNECTION_STRING + "' with a connection string"));
}
connectionString(connectionString);
}
if (proxyOptions == null) {
proxyOptions = ProxyOptions.fromConfiguration(buildConfiguration);
}
// If the proxy has been configured by the user but they have overridden the TransportType with something that
// is not AMQP_WEB_SOCKETS.
if (proxyOptions != null && proxyOptions.isProxyAddressConfigured()
&& transport != AmqpTransportType.AMQP_WEB_SOCKETS) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
"Cannot use a proxy when TransportType is not AMQP Web Sockets. "
+ "Use the setter 'transportType(AmqpTransportType.AMQP_WEB_SOCKETS)' to enable Web Sockets mode."));
}
final CbsAuthorizationType authorizationType = credentials instanceof EventHubSharedKeyCredential
? CbsAuthorizationType.SHARED_ACCESS_SIGNATURE
: CbsAuthorizationType.JSON_WEB_TOKEN;
final SslDomain.VerifyMode verificationMode = verifyMode != null
? verifyMode
: SslDomain.VerifyMode.VERIFY_PEER_NAME;
final ClientOptions options = clientOptions != null ? clientOptions : new ClientOptions();
if (customEndpointAddress == null) {
return new ConnectionOptions(getAndValidateFullyQualifiedNamespace(), credentials, authorizationType,
ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, transport, retryOptions, proxyOptions, scheduler,
options, verificationMode, LIBRARY_NAME, LIBRARY_VERSION);
} else {
return new ConnectionOptions(getAndValidateFullyQualifiedNamespace(), credentials, authorizationType,
ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, transport, retryOptions, proxyOptions, scheduler,
options, verificationMode, LIBRARY_NAME, LIBRARY_VERSION, customEndpointAddress.getHost(),
customEndpointAddress.getPort());
}
}
}