diff --git a/core/azure-core-amqp/README.md b/core/azure-core-amqp/README.md
new file mode 100644
index 000000000000..baf975788a87
--- /dev/null
+++ b/core/azure-core-amqp/README.md
@@ -0,0 +1,42 @@
+# Azure Core AMQP client library for Java
+
+Azure Core AMQP client library is a collection of classes common to the AMQP protocol. It help developers create their
+own AMQP client library that abstracts from the underlying transport library's implementation.
+
+## Getting started
+
+### Prerequisites
+
+- Java Development Kit (JDK) with version 8 or above
+
+### Adding the package to your product
+
+```xml
+
+ com.azure
+ azure-core-amqp
+ 1.0.0-SNAPSHOT
+
+```
+
+## Key concepts
+
+The concepts for AMQP are well documented in [OASIS Advanced Messaging Queuing Protocol (AMQP) Version
+1.0](http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-overview-v1.0-os.html).
+
+## Examples
+
+## Troubleshooting
+
+## Next steps
+
+## Contributing
+
+If you would like to become an active contributor to this project please follow the instructions provided in [Microsoft
+Azure Projects Contribution Guidelines](http://azure.github.io/guidelines.html).
+
+1. Fork it
+1. Create your feature branch (`git checkout -b my-new-feature`)
+1. Commit your changes (`git commit -am 'Add some feature'`)
+1. Push to the branch (`git push origin my-new-feature`)
+1. Create new Pull Request
diff --git a/core/azure-core-amqp/pom.xml b/core/azure-core-amqp/pom.xml
new file mode 100644
index 000000000000..3e94064e0fd7
--- /dev/null
+++ b/core/azure-core-amqp/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ com.azure
+ azure-core-parent
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ com.azure
+ azure-core-amqp
+ 1.0.0-SNAPSHOT
+ jar
+
+ Microsoft Azure Java Core AMQP Library
+ This package contains core types for Azure Java AMQP clients.
+ https://github.com/Azure/azure-sdk-for-java
+
+
+
+ The MIT License (MIT)
+ http://opensource.org/licenses/MIT
+ repo
+
+
+
+
+
+ azure-java-build-docs
+ ${site.url}/site/${project.artifactId}
+
+
+
+
+ scm:git:https://github.com/Azure/azure-sdk-for-java
+
+
+
+ UTF-8
+
+
+
+
+
+ microsoft
+ Microsoft
+
+
+
+
+
+ com.azure
+ azure-core
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
+ junit
+ junit
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java
new file mode 100644
index 000000000000..0c7a5fc1bd11
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.util.Map;
+
+/**
+ * Represents a TCP connection between the client and a service that uses the AMQP protocol.
+ */
+public interface AmqpConnection extends EndpointStateNotifier, Closeable {
+ /**
+ * Gets the connection identifier.
+ *
+ * @return The connection identifier.
+ */
+ String getIdentifier();
+
+ /**
+ * Gets the host for the AMQP connection.
+ *
+ * @return The host for the AMQP connection.
+ */
+ String getHost();
+
+ /**
+ * Gets the maximum frame size for the connection.
+ *
+ * @return The maximum frame size for the connection.
+ */
+ int getMaxFrameSize();
+
+ /**
+ * Gets the connection properties.
+ *
+ * @return Properties associated with this connection.
+ */
+ Map getConnectionProperties();
+
+ /**
+ * Gets the claims-based security (CBS) node that authorizes access to resources.
+ *
+ * @return Provider that authorizes access to AMQP resources.
+ */
+ Mono getCBSNode();
+
+ /**
+ * Creates a new session with the given session name.
+ *
+ * @param sessionName Name of the session.
+ * @return The AMQP session that was created.
+ */
+ Mono createSession(String sessionName);
+
+ /**
+ * Removes a session with the {@code sessionName} from the AMQP connection.
+ *
+ * @param sessionName Name of the session to remove.
+ * @return {@code true} if a session with the name was removed; {@code false} otherwise.
+ */
+ boolean removeSession(String sessionName);
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpEndpointState.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpEndpointState.java
new file mode 100644
index 000000000000..86a078f0f9dc
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpEndpointState.java
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+/**
+ * Represents a state for a connection, session, or link.
+ */
+public enum AmqpEndpointState {
+ /**
+ * The endpoint has not been initialized.
+ */
+ UNINITIALIZED,
+ /**
+ * The endpoint is active.
+ */
+ ACTIVE,
+ /**
+ * The endpoint is closed.
+ */
+ CLOSED
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpExceptionHandler.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpExceptionHandler.java
new file mode 100644
index 000000000000..876c4b46713b
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpExceptionHandler.java
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import com.azure.core.util.logging.ClientLogger;
+
+/**
+ * Handles exceptions generated by AMQP connections, sessions, and/or links.
+ */
+public abstract class AmqpExceptionHandler {
+ private final ClientLogger logger = new ClientLogger(AmqpExceptionHandler.class);
+
+ /**
+ * Creates a new instance of the exception handler.
+ */
+ protected AmqpExceptionHandler() {
+ }
+
+ /**
+ * Notifies the exception handler of an exception.
+ *
+ * @param exception The exception that caused the connection error.
+ */
+ public void onConnectionError(Throwable exception) {
+ logger.asWarning().log("Connection exception encountered: " + exception.toString(), exception);
+ }
+
+ /**
+ * Notifies the exception handler that a shutdown signal occurred.
+ *
+ * @param shutdownSignal The shutdown signal that was received.
+ */
+ public void onConnectionShutdown(AmqpShutdownSignal shutdownSignal) {
+ logger.asInfo().log("Shutdown received: {}", shutdownSignal);
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java
new file mode 100644
index 000000000000..f696cf6f7840
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import java.io.Closeable;
+
+/**
+ * Represents a unidirectional AMQP link.
+ */
+public interface AmqpLink extends EndpointStateNotifier, Closeable {
+ /**
+ * Gets the name of the link.
+ *
+ * @return The name of the link.
+ */
+ String getLinkName();
+
+ /**
+ * The remote endpoint path this link is connected to.
+ *
+ * @return The remote endpoint path this link is connected to.
+ */
+ String getEntityPath();
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java
new file mode 100644
index 000000000000..2ee596fd0390
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.time.Duration;
+
+/**
+ * An AMQP session representing bidirectional communication that supports multiple {@link AmqpLink AMQP links}.
+ */
+public interface AmqpSession extends EndpointStateNotifier, Closeable {
+ /**
+ * Gets the name for this AMQP session.
+ *
+ * @return The name for the AMQP session.
+ */
+ String getSessionName();
+
+ /**
+ * Gets the operation timeout for starting the AMQP session.
+ *
+ * @return The timeout for starting the AMQP session.
+ */
+ Duration getOperationTimeout();
+
+ /**
+ * Creates a new AMQP link that publishes events to the message broker.
+ *
+ * @param linkName Name of the link.
+ * @param entityPath The entity path this link connects to when producing events.
+ * @param timeout Timeout required for creating and opening AMQP link.
+ * @param retry The retry policy to use when sending messages.
+ * @return A newly created AMQP link.
+ */
+ Mono createProducer(String linkName, String entityPath, Duration timeout, Retry retry);
+
+ /**
+ * Creates a new AMQP link that consumes events from the message broker.
+ *
+ * @param linkName Name of the link.
+ * @param entityPath The entity path this link connects to, so that it may read events from the message broker.
+ * @param timeout Timeout required for creating and opening an AMQP link.
+ * @param retry The retry policy to use when consuming messages.
+ * @return A newly created AMQP link.
+ */
+ Mono createConsumer(String linkName, String entityPath, Duration timeout, Retry retry);
+
+ /**
+ * Removes an {@link AmqpLink} with the given {@code linkName}.
+ *
+ * @param linkName Name of the link to remove.
+ * @return {@code true} if the link was removed; {@code false} otherwise.
+ */
+ boolean removeLink(String linkName);
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpShutdownSignal.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpShutdownSignal.java
new file mode 100644
index 000000000000..59107914d55a
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpShutdownSignal.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import java.util.Locale;
+
+/**
+ * Represents a signal that caused the AMQP connection to shutdown.
+ */
+public class AmqpShutdownSignal {
+ private final boolean isTransient;
+ private final boolean isInitiatedByClient;
+ private final String message;
+
+ /**
+ * Creates a new instance of the AmqpShutdownSignal.
+ *
+ * @param isTransient Whether the shutdown signal can be retried or not.
+ * @param isInitiatedByClient {@code true} if the shutdown was initiated by the client; {@code false} otherwise.
+ * @param message Message associated with the shutdown.
+ */
+ public AmqpShutdownSignal(boolean isTransient, boolean isInitiatedByClient, String message) {
+ this.isTransient = isTransient;
+ this.isInitiatedByClient = isInitiatedByClient;
+ this.message = message;
+ }
+
+ /**
+ * Gets whether or not this shutdown signal is transient or if it can be restarted.
+ *
+ * @return {@code true} if the shutdown signal is transient and the connection, session, or link can be recreated.
+ * {@code false} otherwise.
+ */
+ public boolean isTransient() {
+ return isTransient;
+ }
+
+ /**
+ * Gets whether or not this shutdown signal was initiated by the client.
+ *
+ * @return {@code true} if the shutdown signal was initiated by the client, {@code false} if the shutdown signal
+ * occurred in the underlying AMQP layer or from the AMQP message broker.
+ */
+ public boolean isInitiatedByClient() {
+ return isInitiatedByClient;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "%s, isTransient[%s], initiatedByClient[%s]", message, isTransient, isInitiatedByClient);
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/CBSNode.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/CBSNode.java
new file mode 100644
index 000000000000..93aa8e746ecc
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/CBSNode.java
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.time.OffsetDateTime;
+
+/**
+ * Claims-based security (CBS) node that authorizes connections with AMQP services.
+ *
+ * @see
+ * AMPQ Claims-based Security v1.0
+ */
+public interface CBSNode extends EndpointStateNotifier, Closeable {
+ /**
+ * Authorizes the caller with the CBS node to access resources for the {@code audience}.
+ *
+ * @param audience Resource that the callee needs access to.
+ * @return A Mono that completes with the callee's expiration date if it is successful and errors if
+ * authorization was unsuccessful. Once the expiration date has elapsed, the callee needs to reauthorize with the
+ * CBS node.
+ */
+ Mono authorize(String audience);
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/EndpointStateNotifier.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/EndpointStateNotifier.java
new file mode 100644
index 000000000000..56ebb9b50ee2
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/EndpointStateNotifier.java
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import reactor.core.publisher.Flux;
+
+/**
+ * Notifies subscribers of the endpoint state and any errors that occur with the object.
+ */
+public interface EndpointStateNotifier {
+
+ /**
+ * Gets the current state of the endpoint.
+ *
+ * @return The current state of the endpoint.
+ */
+ AmqpEndpointState getCurrentState();
+
+ /**
+ * Gets the errors that occurred in the AMQP endpoint.
+ *
+ * @return A stream of errors that occurred in the AMQP endpoint.
+ */
+ Flux getErrors();
+
+ /**
+ * Gets the endpoint states for the AMQP endpoint.
+ *
+ * @return A stream of endpoint states as they occur in the endpoint.
+ */
+ Flux getConnectionStates();
+
+ /**
+ * Gets any shutdown signals that occur in the AMQP endpoint.
+ *
+ * @return A stream of shutdown signals that occur in the AMQP endpoint.
+ */
+ Flux getShutdownSignals();
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ExponentialRetry.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ExponentialRetry.java
new file mode 100644
index 000000000000..3e86faa719c8
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ExponentialRetry.java
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import java.time.Duration;
+
+/**
+ * A policy to govern retrying of messaging operations in which the delay between retries
+ * will grow in an exponential manner, allowing more time to recover as the number of retries increases.
+ */
+public final class ExponentialRetry extends Retry {
+ private static final Duration TIMER_TOLERANCE = Duration.ofSeconds(1);
+
+ private final Duration minBackoff;
+ private final Duration maxBackoff;
+ private final double retryFactor;
+
+ /**
+ * Creates a new instance with a minimum and maximum retry period in addition to maximum number of retry attempts.
+ *
+ * @param minBackoff The minimum time period permissible for backing off between retries.
+ * @param maxBackoff The maximum time period permissible for backing off between retries.
+ * @param maxRetryCount The maximum number of retries allowed.
+ */
+ public ExponentialRetry(Duration minBackoff, Duration maxBackoff, int maxRetryCount) {
+ super(maxRetryCount);
+ this.minBackoff = minBackoff;
+ this.maxBackoff = maxBackoff;
+
+ this.retryFactor = computeRetryFactor();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected Duration calculateNextRetryInterval(final Exception lastException,
+ final Duration remainingTime,
+ final int baseWaitSeconds,
+ final int retryCount) {
+ final double nextRetryInterval = Math.pow(retryFactor, (double) retryCount);
+ final long nextRetryIntervalSeconds = (long) nextRetryInterval;
+ final long nextRetryIntervalNano = (long) ((nextRetryInterval - (double) nextRetryIntervalSeconds) * 1000000000);
+
+ if (remainingTime.getSeconds() < Math.max(nextRetryInterval, TIMER_TOLERANCE.getSeconds())) {
+ return null;
+ }
+
+ final Duration retryAfter = minBackoff.plus(Duration.ofSeconds(nextRetryIntervalSeconds, nextRetryIntervalNano));
+ return retryAfter.plus(Duration.ofSeconds(baseWaitSeconds));
+ }
+
+ private double computeRetryFactor() {
+ final long deltaBackoff = maxBackoff.minus(minBackoff).getSeconds();
+ if (deltaBackoff <= 0 || super.getMaxRetryCount() <= 0) {
+ return 0;
+ }
+ return Math.log(deltaBackoff) / Math.log(super.getMaxRetryCount());
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/MessageConstant.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/MessageConstant.java
new file mode 100644
index 000000000000..2f769d5cf5ac
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/MessageConstant.java
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Reserved well-known constants from AMQP protocol.
+ *
+ * @see
+ * AMQP 1.0: Messaging Properties
+ */
+public enum MessageConstant {
+ /**
+ * Message-id, if set, uniquely identifies a message within the message system. The message producer is usually
+ * responsible for setting the message-id in such a way that it is assured to be globally unique. A broker MAY
+ * discard a message as a duplicate if the value of the message-id matches that of a previously received message
+ * sent to the same node.
+ */
+ MESSAGE_ID("message-id"),
+ /**
+ * The identity of the user responsible for producing the message. The client sets this value, and it MAY be
+ * authenticated by intermediaries.
+ */
+ USER_ID("user-id"),
+ /**
+ * The to field identifies the node that is the intended destination of the message. On any given transfer this
+ * might not be the node at the receiving end of the link.
+ */
+ TO("to"),
+ /**
+ * A common field for summary information about the message content and purpose.
+ */
+ SUBJECT("subject"),
+ /**
+ * The address of the node to send replies to.
+ */
+ REPLY_TO("reply-to"),
+ /**
+ * This is a client-specific id that can be used to mark or identify messages between clients.
+ */
+ CORRELATION_ID("correlation-id"),
+ /**
+ * The RFC-2046 MIME type for the message's application-data section (body). As per RFC-2046 this can contain a
+ * charset parameter defining the character encoding used: e.g., 'text/plain), charset="utf-8"'.
+ */
+ CONTENT_TYPE("content-type"),
+ /**
+ * The content-encoding property is used as a modifier to the content-type. When present, its value indicates what
+ * additional content encodings have been applied to the application-data, and thus what decoding mechanisms need to
+ * be applied in order to obtain the media-type referenced by the content-type header field.
+ */
+ CONTENT_ENCODING("content-encoding"),
+ /**
+ * An absolute time when this message is considered to be expired.
+ */
+ ABSOLUTE_EXPRITY_TIME("absolute-expiry-time"),
+ /**
+ * An absolute time when this message was created.
+ */
+ CREATION_TIME("creation-time"),
+ /**
+ * Identifies the group the message belongs to.
+ */
+ GROUP_ID("group-id"),
+ /**
+ * The relative position of this message within its group.
+ */
+ GROUP_SEQUENCE("group-sequence"),
+ /**
+ * This is a client-specific id that is used so that client can send replies to this message to a specific group.
+ */
+ REPLY_TO_GROUP_ID("reply-to-group-id"),
+ /**
+ * The offset of a message within a given partition.
+ */
+ OFFSET_ANNOTATION_NAME("x-opt-offset"),
+ /**
+ * The date and time, in UTC, that a message was enqueued.
+ */
+ ENQUEUED_TIME_UTC_ANNOTATION_NAME("x-opt-enqueued-time"),
+ /**
+ * The identifier associated with a given partition.
+ */
+ PARTITION_KEY_ANNOTATION_NAME("x-opt-partition-key"),
+ /**
+ * The sequence number assigned to a message.
+ */
+ SEQUENCE_NUMBER_ANNOTATION_NAME("x-opt-sequence-number"),
+ /**
+ * The name of the entity that published a message.
+ */
+ PUBLISHER_ANNOTATION_NAME("x-opt-publisher");
+
+ private static final Map RESERVED_CONSTANTS_MAP = new HashMap<>();
+ private final String constant;
+
+ static {
+ for (MessageConstant error : MessageConstant.values()) {
+ RESERVED_CONSTANTS_MAP.put(error.getValue(), error);
+ }
+ }
+
+ MessageConstant(String value) {
+ this.constant = value;
+ }
+
+ /**
+ * Gets the AMQP messaging header value.
+ *
+ * @return The AMQP header value for this messaging constant.
+ */
+ public String getValue() {
+ return constant;
+ }
+
+ /**
+ * Parses an header value to its message constant.
+ *
+ * @param headerValue the messaging header value to parse.
+ * @return the parsed MessageConstant object, or {@code null} if unable to parse.
+ * @throws NullPointerException if {@code constant} is {@code null}.
+ */
+ public static MessageConstant fromString(String headerValue) {
+ Objects.requireNonNull(headerValue);
+
+ return RESERVED_CONSTANTS_MAP.get(headerValue);
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/Retry.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/Retry.java
new file mode 100644
index 000000000000..39f969e8bdd7
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/Retry.java
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorCondition;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An abstract representation of a policy to govern retrying of messaging operations.
+ */
+public abstract class Retry {
+ /**
+ * Default for the minimum time between retry attempts.
+ */
+ public static final Duration DEFAULT_RETRY_MIN_BACKOFF = Duration.ofSeconds(0);
+ /**
+ * Default for the maximum time between retry attempts.
+ */
+ public static final Duration DEFAULT_RETRY_MAX_BACKOFF = Duration.ofSeconds(30);
+ /**
+ * Default for the maximum number of retry attempts.
+ */
+ public static final int DEFAULT_MAX_RETRY_COUNT = 10;
+
+ /**
+ * Base sleep wait time.
+ */
+ private static final int SERVER_BUSY_BASE_SLEEP_TIME_IN_SECS = 4;
+
+ private final AtomicInteger retryCount = new AtomicInteger();
+ private final int maxRetryCount;
+
+ /**
+ * Creates a new instance of Retry with the maximum retry count of {@code maxRetryCount}
+ *
+ * @param maxRetryCount The maximum number of retries allowed.
+ */
+ public Retry(int maxRetryCount) {
+ this.maxRetryCount = maxRetryCount;
+ }
+
+ /**
+ * Check if the existing exception is a retriable exception.
+ *
+ * @param exception An exception that was observed for the operation to be retried.
+ * @return true if the exception is a retriable exception, otherwise false.
+ */
+ public static boolean isRetriableException(Exception exception) {
+ return (exception instanceof AmqpException) && ((AmqpException) exception).isTransient();
+ }
+
+ /**
+ * Creates a Retry policy that does not retry failed requests.
+ *
+ * @return A new Retry policy that does not retry failed requests.
+ */
+ public static Retry getNoRetry() {
+ return new ExponentialRetry(Duration.ZERO, Duration.ZERO, 0);
+ }
+
+ /**
+ * Creates a Retry policy that retries failed requests up to {@link #DEFAULT_MAX_RETRY_COUNT 10} times. As the
+ * number of retry attempts increase, the period between retry attempts increases.
+ *
+ * @return A new instance with the default Retry values configured.
+ */
+ public static Retry getDefaultRetry() {
+ return new ExponentialRetry(DEFAULT_RETRY_MIN_BACKOFF, DEFAULT_RETRY_MAX_BACKOFF, DEFAULT_MAX_RETRY_COUNT);
+ }
+
+ /**
+ * Increments the number of retry attempts and returns the previous number of retry counts.
+ *
+ * @return The number of retry attempts before it was incremented.
+ */
+ public int incrementRetryCount() {
+ return retryCount.getAndIncrement();
+ }
+
+ /**
+ * Gets the current number of retry attempts for this instance.
+ *
+ * @return The current number of retry attempts.
+ */
+ public int getRetryCount() {
+ return retryCount.get();
+ }
+
+ /**
+ * Resets the number of retry attempts for this instance.
+ */
+ public void resetRetryInterval() {
+ retryCount.set(0);
+ }
+
+ /**
+ * Gets the maximum number of retry attempts that are allowed.
+ *
+ * @return The maximum number of retry attempts.
+ */
+ public int getMaxRetryCount() {
+ return maxRetryCount;
+ }
+
+ /**
+ * Calculates the amount of time to delay before the next retry attempt.
+ *
+ * @param lastException The last exception that was observed for the operation to be retried.
+ * @param remainingTime The amount of time remaining for the cumulative timeout across retry attempts.
+ * @return The amount of time to delay before retrying the associated operation; if {@code null},
+ * then the operation is no longer eligible to be retried.
+ */
+ public Duration getNextRetryInterval(Exception lastException, Duration remainingTime) {
+ int baseWaitTime = 0;
+
+ if (!isRetriableException(lastException) || retryCount.get() >= maxRetryCount) {
+ return null;
+ }
+
+ if (!(lastException instanceof AmqpException)) {
+ return null;
+ }
+
+ if (((AmqpException) lastException).getErrorCondition() == ErrorCondition.SERVER_BUSY_ERROR) {
+ baseWaitTime += SERVER_BUSY_BASE_SLEEP_TIME_IN_SECS;
+ }
+
+ return this.calculateNextRetryInterval(lastException, remainingTime, baseWaitTime, this.getRetryCount());
+ }
+
+ /**
+ * Allows a concrete retry policy implementation to offer a base retry interval to be used in
+ * the calculations performed by 'Retry.GetNextRetryInterval'.
+ *
+ * @param lastException The last exception that was observed for the operation to be retried.
+ * @param remainingTime The amount of time remaining for the cumulative timeout across retry attempts.
+ * @param baseWaitSeconds The number of seconds to base the suggested retry interval on;
+ * this should be used as the minimum interval returned under normal circumstances.
+ * @param retryCount The number of retries that have already been attempted.
+ * @return The amount of time to delay before retrying the associated operation; if {@code null},
+ * then the operation is no longer eligible to be retried.
+ */
+ protected abstract Duration calculateNextRetryInterval(Exception lastException, Duration remainingTime,
+ int baseWaitSeconds, int retryCount);
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/TransportType.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/TransportType.java
new file mode 100644
index 000000000000..cd0b53697aca
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/TransportType.java
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import java.util.Locale;
+
+/**
+ * All TransportType switches available for AMQP protocol.
+ */
+public enum TransportType {
+ /**
+ * AMQP over TCP. Uses port 5671 - assigned by IANA for secure AMQP (AMQPS).
+ */
+ AMQP("Amqp"),
+
+ /**
+ * AMQP over Web Sockets. Uses port 443.
+ */
+ AMQP_WEB_SOCKETS("AmqpWebSockets");
+
+ private final String value;
+
+ TransportType(final String value) {
+ this.value = value;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return this.value;
+ }
+
+ /**
+ * Creates an TransportType from its display value.
+ *
+ * @param value The string value of the TransportType.
+ * @return The TransportType represented by the value.
+ * @throws IllegalArgumentException If a TransportType cannot be parsed from the string value.
+ */
+ public static TransportType fromString(final String value) {
+ for (TransportType transportType : values()) {
+ if (transportType.value.equalsIgnoreCase(value)) {
+ return transportType;
+ }
+ }
+
+ throw new IllegalArgumentException(String.format(Locale.US, "Could not convert %s to a TransportType", value));
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpException.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpException.java
new file mode 100644
index 000000000000..67cbc344929b
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpException.java
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import com.azure.core.exception.AzureException;
+import com.azure.core.implementation.util.ImplUtils;
+
+import java.util.Locale;
+
+/**
+ * General exception for AMQP related failures.
+ *
+ * @see ErrorCondition
+ * @see Azure Messaging Exceptions
+ */
+public class AmqpException extends AzureException {
+ private static final long serialVersionUID = -3654294093967132325L;
+
+ private final ErrorContext errorContext;
+ private final boolean isTransient;
+ private final ErrorCondition errorCondition;
+
+ /**
+ * Initializes a new instance of the AmqpException class.
+ *
+ * @param isTransient A boolean indicating if the exception is a transient error or not. If true, then the request
+ * can be retried; otherwise not.
+ * @param message Text containing any supplementary details of the exception.
+ * @param errorContext The context that caused this AMQP error.
+ */
+ public AmqpException(boolean isTransient, String message, ErrorContext errorContext) {
+ this(isTransient, null, message, errorContext);
+ }
+
+ /**
+ * Initializes a new instance of the AmqpException class.
+ *
+ * @param isTransient A boolean indicating if the exception is a transient error or not. If true, then the request
+ * can be retried; otherwise not.
+ * @param errorCondition The symbolic value indicating the error condition.
+ * @param message Text containing any supplementary details not indicated by the condition field. This text can
+ * be logged as an aid to resolving issues.
+ * @param errorContext The context that caused this AMQP error.
+ */
+ public AmqpException(boolean isTransient, ErrorCondition errorCondition, String message, ErrorContext errorContext) {
+ super(message);
+ this.errorCondition = errorCondition;
+ this.isTransient = isTransient;
+ this.errorContext = errorContext;
+ }
+
+ /**
+ * Initializes a new instance of the AmqpException class.
+ *
+ * @param isTransient A boolean indicating if the exception is a transient error or not. If true, then the request
+ * can be retried; otherwise not.
+ * @param errorCondition The symbolic value indicating the error condition.
+ * @param message Text containing any supplementary details not indicated by the condition field. This text can
+ * be logged as an aid to resolving issues.
+ * @param cause The Throwable which caused the creation of this AmqpException.
+ * @param errorContext The context that caused this AMQP error.
+ */
+ public AmqpException(boolean isTransient, ErrorCondition errorCondition, String message, Throwable cause,
+ ErrorContext errorContext) {
+ super(message, cause);
+ this.errorCondition = errorCondition;
+ this.isTransient = isTransient;
+ this.errorContext = errorContext;
+ }
+
+ /**
+ * Initializes a new instance of the AmqpException class.
+ *
+ * @param isTransient A boolean indicating if the exception is a transient error or not. If true, then the request
+ * can be retried; otherwise not.
+ * @param errorCondition The symbolic value indicating the error condition.
+ * @param cause The Throwable which caused the creation of this AmqpException.
+ * @param errorContext The context that caused this AMQP error.
+ */
+ public AmqpException(boolean isTransient, ErrorCondition errorCondition, Throwable cause, ErrorContext errorContext) {
+ super(cause.getMessage(), cause);
+ this.errorCondition = errorCondition;
+ this.isTransient = isTransient;
+ this.errorContext = errorContext;
+ }
+
+ /**
+ * Initializes a new instance of the AmqpException class.
+ *
+ * @param isTransient A boolean indicating if the exception is a transient error or not. If true, then the request
+ * can be retried; otherwise not.
+ * @param message Text containing any supplementary details not indicated by the condition field. This text can
+ * be logged as an aid to resolving issues.
+ * @param cause The Throwable which caused the creation of this AmqpException.
+ * @param errorContext The context that caused this AMQP error.
+ */
+ public AmqpException(boolean isTransient, String message, Throwable cause, ErrorContext errorContext) {
+ super(message, cause);
+ this.errorCondition = null;
+ this.isTransient = isTransient;
+ this.errorContext = errorContext;
+ }
+
+ @Override
+ public String getMessage() {
+ final String baseMessage = super.getMessage();
+
+ if (this.errorContext == null) {
+ return super.getMessage();
+ }
+
+ return !ImplUtils.isNullOrEmpty(baseMessage)
+ ? String.format(Locale.US, "%s, %s[%s]", baseMessage, "errorContext", errorContext.toString())
+ : String.format(Locale.US, "%s[%s]", "errorContext", errorContext.toString());
+ }
+
+ /**
+ * A boolean indicating if the exception is a transient error or not.
+ *
+ * @return returns true when user can retry the operation that generated the exception without additional intervention.
+ */
+ public boolean isTransient() {
+ return this.isTransient;
+ }
+
+ /**
+ * Gets the ErrorCondition for this exception.
+ *
+ * @return The ErrorCondition for this exception, or {@code null} if nothing was set.
+ */
+ public ErrorCondition getErrorCondition() {
+ return this.errorCondition;
+ }
+
+ /**
+ * Gets the context for this exception.
+ *
+ * @return The context for this exception.
+ */
+ public ErrorContext getContext() {
+ return this.errorContext;
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpResponseCode.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpResponseCode.java
new file mode 100644
index 000000000000..6b78e2fcfd87
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/AmqpResponseCode.java
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Error response codes returned from AMQP.
+ */
+public enum AmqpResponseCode {
+ ACCEPTED(0xca),
+ OK(200),
+ BAD_REQUEST(400),
+ NOT_FOUND(0x194),
+ FORBIDDEN(0x193),
+ INTERNAL_SERVER_ERROR(500),
+ UNAUTHORIZED(0x191);
+
+ private static Map valueMap = new HashMap<>();
+
+ static {
+ for (AmqpResponseCode code : AmqpResponseCode.values()) {
+ valueMap.put(code.value, code);
+ }
+ }
+
+ private final int value;
+
+ AmqpResponseCode(final int value) {
+ this.value = value;
+ }
+
+ /**
+ * Creates an AmqpResponseCode for the provided integer {@code value}.
+ *
+ * @param value The integer value representing an error code.
+ * @return The corresponding AmqpResponseCode for the provided value, or {@code null} if no matching response code
+ * is found.
+ */
+ public static AmqpResponseCode fromValue(final int value) {
+ return valueMap.get(value);
+ }
+
+ /**
+ * Gets the integer value of the AmqpResponseCode
+ *
+ * @return The integer value of the AmqpResponseCode
+ */
+ public int getValue() {
+ return this.value;
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorCondition.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorCondition.java
new file mode 100644
index 000000000000..0a36df50653f
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorCondition.java
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Contains constants common to the AMQP protocol and constants shared by Azure services.
+ *
+ * @see AMQP
+ * 1.0: Transport Errors
+ * @see Azure Messaging Exceptions
+ */
+public enum ErrorCondition {
+ /**
+ * A peer attempted to work with a remote entity that does not exist.
+ */
+ NOT_FOUND("amqp:not-found"),
+ /**
+ * A peer attempted to work with a remote entity to which it has no access due to security settings.
+ */
+ UNAUTHORIZED_ACCESS("amqp:unauthorized-access"),
+ /**
+ * A peer exceeded its resource allocation.
+ */
+ RESOURCE_LIMIT_EXCEEDED("amqp:resource-limit-exceeded"),
+ /**
+ * The peer tried to use a frame in a manner that is inconsistent with the semantics defined in the specification.
+ */
+ NOT_ALLOWED("amqp:not-allowed"),
+ /**
+ * An internal error occurred. Operator intervention might be necessary to resume normal operation.
+ */
+ INTERNAL_ERROR("amqp:internal-error"),
+ /**
+ * The peer sent a frame that is not permitted in the current state.
+ */
+ ILLEGAL_STATE("amqp:illegal-state"),
+ /**
+ * The peer tried to use functionality that is not implemented in its partner.
+ */
+ NOT_IMPLEMENTED("amqp:not-implemented"),
+
+ /**
+ * The link has been attached elsewhere, causing the existing attachment to be forcibly closed.
+ */
+ LINK_STOLEN("amqp:link:stolen"),
+ /**
+ * The peer sent a larger message than is supported on the link.
+ */
+ LINK_PAYLOAD_SIZE_EXCEEDED("amqp:link:message-size-exceeded"),
+ /**
+ * An operator intervened to detach for some reason.
+ */
+ LINK_DETACH_FORCED("amqp:link:detach-forced"),
+
+ /**
+ * An operator intervened to close the connection for some reason. The client could retry at some later date.
+ */
+ CONNECTION_FORCED("amqp:connection:forced"),
+
+ // These are errors that are specific to Azure services.
+ SERVER_BUSY_ERROR("com.microsoft:server-busy"),
+ /**
+ * One or more arguments supplied to the method are invalid.
+ */
+ ARGUMENT_ERROR("com.microsoft:argument-error"),
+ /**
+ * One or more arguments supplied to the method are invalid.
+ */
+ ARGUMENT_OUT_OF_RANGE_ERROR("com.microsoft:argument-out-of-range"),
+ /**
+ * Request for a runtime operation on a disabled entity.
+ */
+ ENTITY_DISABLED_ERROR("com.microsoft:entity-disabled"),
+ /**
+ * Partition is not owned.
+ */
+ PARTITION_NOT_OWNED_ERROR("com.microsoft:partition-not-owned"),
+ /**
+ * Lock token associated with the message or session has expired, or the lock token is not found.
+ */
+ STORE_LOCK_LOST_ERROR("com.microsoft:store-lock-lost"),
+ /**
+ * The TokenProvider object could not acquire a token, the token is invalid, or the token does not contain the
+ * claims required to perform the operation.
+ */
+ PUBLISHER_REVOKED_ERROR("com.microsoft:publisher-revoked"),
+ /**
+ * The server did not respond to the requested operation within the specified time. The server may have completed
+ * the requested operation. This can happen due to network or other infrastructure delays.
+ */
+ TIMEOUT_ERROR("com.microsoft:timeout"),
+ /**
+ * Tracking Id for an exception.
+ */
+ TRACKING_ID_PROPERTY("com.microsoft:tracking-id");
+
+ private static final Map ERROR_CONSTANT_MAP = new HashMap<>();
+ private final String errorCondition;
+
+ static {
+ for (ErrorCondition error : ErrorCondition.values()) {
+ ERROR_CONSTANT_MAP.put(error.getErrorCondition(), error);
+ }
+ }
+
+ ErrorCondition(String errorCondition) {
+ this.errorCondition = errorCondition;
+ }
+
+ /**
+ * Gets the AMQP header value for this error condition.
+ *
+ * @return AMQP header value for this error condition.
+ */
+ public String getErrorCondition() {
+ return errorCondition;
+ }
+
+ /**
+ * Parses a serialized value to an ErrorCondition instance.
+ *
+ * @param errorCondition the serialized value to parse.
+ * @return the parsed ErrorCondition object, or null if unable to parse.
+ * @throws NullPointerException if {@code errorCondition} is {@code null}.
+ */
+ public static ErrorCondition fromString(String errorCondition) {
+ Objects.requireNonNull(errorCondition);
+
+ return ERROR_CONSTANT_MAP.get(errorCondition);
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorContext.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorContext.java
new file mode 100644
index 000000000000..6bf5776ac742
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ErrorContext.java
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.core.amqp.exception;
+
+import com.azure.core.amqp.AmqpConnection;
+import com.azure.core.amqp.AmqpLink;
+import com.azure.core.amqp.AmqpSession;
+import com.azure.core.implementation.util.ImplUtils;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+/**
+ * Provides context for an {@link AmqpException} that occurs in an {@link AmqpConnection}, {@link AmqpSession},
+ * or {@link AmqpLink}.
+ *
+ * @see AmqpException
+ * @see SessionErrorContext
+ * @see LinkErrorContext
+ */
+public class ErrorContext implements Serializable {
+ static final String MESSAGE_PARAMETER_DELIMITER = ", ";
+
+ private static final long serialVersionUID = -2819764407122954922L;
+
+ private final String namespace;
+
+ /**
+ * Creates a new instance with the provided {@code namespace}.
+ *
+ * @param namespace The service namespace of the error.
+ * @throws IllegalArgumentException when {@code namespace} is {@code null} or empty.
+ */
+ public ErrorContext(String namespace) {
+ if (ImplUtils.isNullOrEmpty(namespace)) {
+ throw new IllegalArgumentException("'namespace' cannot be null or empty");
+ }
+
+ this.namespace = namespace;
+ }
+
+ /**
+ * Gets the namespace for this error.
+ *
+ * @return The namespace for this error.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * Creates a string representation of this ErrorContext.
+ *
+ * @return A string representation of this ErrorContext.
+ */
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "NAMESPACE: %s", getNamespace());
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ExceptionUtil.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ExceptionUtil.java
new file mode 100644
index 000000000000..4033340f1185
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/ExceptionUtil.java
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to map AMQP status codes and error conditions to an exception.
+ */
+public final class ExceptionUtil {
+ private static final String AMQP_REQUEST_FAILED_ERROR = "status-code: %s, status-description: %s";
+ private static final Pattern ENTITY_NOT_FOUND_PATTERN = Pattern.compile("The messaging entity .* could not be found");
+
+ /**
+ * Creates an {@link AmqpException} or Exception based on the {@code errorCondition} from the AMQP request.
+ *
+ * @param errorCondition The error condition string.
+ * @param description The error message.
+ * @param errorContext The context that this error occurred in.
+ * @return An exception that maps to the {@code errorCondition} provided.
+ * @throws IllegalArgumentException when 'errorCondition' is {@code null} or empty, cannot be translated into an
+ * {@link ErrorCondition}, or cannot be determined whether the {@link ErrorCondition} is transient or not.
+ * @see ErrorCondition
+ */
+ public static Exception toException(String errorCondition, String description, ErrorContext errorContext) {
+ if (errorCondition == null) {
+ throw new IllegalArgumentException("'null' errorCondition cannot be translated to EventHubException");
+ }
+
+ final ErrorCondition condition = ErrorCondition.fromString(errorCondition);
+
+ if (condition == null) {
+ throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not a known ErrorCondition.", errorCondition));
+ }
+
+ boolean isTransient;
+ switch (condition) {
+ case TIMEOUT_ERROR:
+ case SERVER_BUSY_ERROR:
+ case INTERNAL_ERROR:
+ case LINK_DETACH_FORCED:
+ case CONNECTION_FORCED:
+ isTransient = true;
+ break;
+ case ENTITY_DISABLED_ERROR:
+ case LINK_STOLEN:
+ case UNAUTHORIZED_ACCESS:
+ case LINK_PAYLOAD_SIZE_EXCEEDED:
+ case ARGUMENT_ERROR:
+ case ARGUMENT_OUT_OF_RANGE_ERROR:
+ case PARTITION_NOT_OWNED_ERROR:
+ case STORE_LOCK_LOST_ERROR:
+ case RESOURCE_LIMIT_EXCEEDED:
+ isTransient = false;
+ break;
+ case NOT_IMPLEMENTED:
+ case NOT_ALLOWED:
+ return new UnsupportedOperationException(description);
+ case NOT_FOUND:
+ return distinguishNotFound(description, errorContext);
+ default:
+ throw new IllegalArgumentException(String.format(Locale.ROOT, "This condition '%s' is not known.", condition));
+ }
+
+ return new AmqpException(isTransient, condition, description, errorContext);
+ }
+
+ /**
+ * Given an AMQP response code, it maps it to an exception.
+ *
+ * @param statusCode AMQP response code.
+ * @param statusDescription Message associated with response.
+ * @param errorContext The context that this error occurred in.
+ * @return An exception that maps to that status code.
+ */
+ public static Exception amqpResponseCodeToException(int statusCode, String statusDescription,
+ ErrorContext errorContext) {
+ final AmqpResponseCode amqpResponseCode = AmqpResponseCode.fromValue(statusCode);
+ final String message = String.format(AMQP_REQUEST_FAILED_ERROR, statusCode, statusDescription);
+
+ if (amqpResponseCode == null) {
+ return new AmqpException(true, message, errorContext);
+ }
+
+ switch (amqpResponseCode) {
+ case BAD_REQUEST:
+ return new IllegalArgumentException(message);
+ case NOT_FOUND:
+ return distinguishNotFound(statusDescription, errorContext);
+ case FORBIDDEN:
+ return new AmqpException(false, ErrorCondition.RESOURCE_LIMIT_EXCEEDED, message, errorContext);
+ case UNAUTHORIZED:
+ return new AmqpException(false, ErrorCondition.UNAUTHORIZED_ACCESS, message, errorContext);
+ default:
+ return new AmqpException(true, message, errorContext);
+ }
+ }
+
+ private static AmqpException distinguishNotFound(String message, ErrorContext errorContext) {
+ final Matcher m = ENTITY_NOT_FOUND_PATTERN.matcher(message);
+ if (m.find()) {
+ return new AmqpException(false, ErrorCondition.NOT_FOUND, message, errorContext);
+ } else {
+ return new AmqpException(true, ErrorCondition.NOT_FOUND,
+ String.format(AMQP_REQUEST_FAILED_ERROR, AmqpResponseCode.NOT_FOUND, message),
+ errorContext);
+ }
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/LinkErrorContext.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/LinkErrorContext.java
new file mode 100644
index 000000000000..91398212abe7
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/LinkErrorContext.java
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import java.util.Locale;
+
+/**
+ * Represents the context for an AMQP link when an {@link AmqpException} occurs.
+ *
+ * @see AmqpException
+ * @see ErrorContext
+ */
+public class LinkErrorContext extends SessionErrorContext {
+ private static final long serialVersionUID = 2581371351997722504L;
+
+ private final String trackingId;
+ private final Integer linkCredit;
+
+ /**
+ * Creates a new instance with the AMQP link's {@code namespace} and {@code entityPath} information. Allows for
+ * optional information about the link if it was successfully opened such as {@code linkCredit} and
+ * {@code trackingId}.
+ *
+ * @param namespace The service namespace of the error context.
+ * @param entityPath The remote container the AMQP receive link is fetching messages from.
+ * @param trackingId The tracking id for the error. Tracking id can be {@code null} if the error was not thrown from
+ * the remote AMQP message broker.
+ * @param linkCredit the number of link credits the current AMQP link has when this error occurred, can be
+ * {@code null} if the receive link has not opened yet.
+ * @throws IllegalArgumentException if {@code namespace} or {@code entityPath} is {@code null} or empty.
+ */
+ public LinkErrorContext(String namespace, String entityPath, String trackingId, Integer linkCredit) {
+ super(namespace, entityPath);
+
+ this.trackingId = trackingId;
+ this.linkCredit = linkCredit;
+ }
+
+ /**
+ * Gets the unique tracking identifier for this error. It is possible to be {@code null} if the error was not thrown
+ * from the AMQP message broker.
+ *
+ * @return The unique tracking identifier for this error.
+ */
+ public String getTrackingId() {
+ return trackingId;
+ }
+
+ /**
+ * Gets the number of credits on the link when the error occurred. Can be {@code null} if the link is not opened.
+ *
+ * @return The number of credits on the link when the error occurred.
+ */
+ public Integer getLinkCredit() {
+ return linkCredit;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder(super.toString());
+
+ if (getTrackingId() != null) {
+ builder.append(MESSAGE_PARAMETER_DELIMITER);
+ builder.append(String.format(Locale.US, "REFERENCE_ID: %s", getTrackingId()));
+ }
+
+ if (getLinkCredit() != null) {
+ builder.append(MESSAGE_PARAMETER_DELIMITER);
+ builder.append(String.format(Locale.US, "LINK_CREDIT: %s", getLinkCredit()));
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/OperationCancelledException.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/OperationCancelledException.java
new file mode 100644
index 000000000000..ca087cd9e0ce
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/OperationCancelledException.java
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+/**
+ * This exception is thrown when the underlying AMQP layer encounters an abnormal link abort or the connection is
+ * disconnected in an unexpected fashion.
+ */
+public class OperationCancelledException extends AmqpException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates an instance of this exception with provided {@code message}.
+ *
+ * @param message Message associated with this exception.
+ * @param context The context that caused this OperationCancelledException.
+ */
+ public OperationCancelledException(String message, ErrorContext context) {
+ super(false, message, context);
+ }
+
+ /**
+ * Creates an instance of this exception with provided {@code message} and underlying {@code cause}.
+ *
+ * @param message Message associated with this exception.
+ * @param cause The throwable that caused this exception to be thrown.
+ * @param context The context that caused this OperationCancelledException.
+ */
+ public OperationCancelledException(final String message, final Throwable cause, ErrorContext context) {
+ super(false, message, cause, context);
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/SessionErrorContext.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/SessionErrorContext.java
new file mode 100644
index 000000000000..098d25ea6009
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/SessionErrorContext.java
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import com.azure.core.implementation.util.ImplUtils;
+
+import java.util.Locale;
+
+/**
+ * Context for an error that occurs in an AMQP session when an {@link AmqpException} occurs.
+ */
+public class SessionErrorContext extends ErrorContext {
+ private static final long serialVersionUID = -6595933736672371232L;
+ private final String entityPath;
+
+ /**
+ * Creates a new instance with the {@code namespace} and {@code entityPath}.
+ *
+ * @param namespace The service namespace of the error.
+ * @param entityPath The remote endpoint this AMQP session is connected to when the error occurred.
+ * @throws IllegalArgumentException if {@code namespace} or {@code entityPath} is {@code null} or empty.
+ */
+ public SessionErrorContext(String namespace, String entityPath) {
+ super(namespace);
+ if (ImplUtils.isNullOrEmpty(entityPath)) {
+ throw new IllegalArgumentException("'entityPath' cannot be null or empty");
+ }
+
+ this.entityPath = entityPath;
+ }
+
+ /**
+ * Gets the remote path this AMQP entity was connected to when the error occurred.
+ *
+ * @return the remote path this AMQP entity was connected to when the error occurred.
+ */
+ public String getEntityPath() {
+ return entityPath;
+ }
+
+ @Override
+ public String toString() {
+ return String.join(MESSAGE_PARAMETER_DELIMITER, super.toString(),
+ String.format(Locale.US, "PATH: %s", getEntityPath()));
+ }
+}
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/package-info.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/package-info.java
new file mode 100644
index 000000000000..c566a417b8d1
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/exception/package-info.java
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Package contains classes related to AMQP exceptions.
+ */
+package com.azure.core.amqp.exception;
diff --git a/core/azure-core-amqp/src/main/java/com/azure/core/amqp/package-info.java b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/package-info.java
new file mode 100644
index 000000000000..97b600ef1900
--- /dev/null
+++ b/core/azure-core-amqp/src/main/java/com/azure/core/amqp/package-info.java
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Package contains classes common to AMQP protocol.
+ */
+package com.azure.core.amqp;
diff --git a/core/azure-core-amqp/src/test/java/com/azure/core/amqp/RetryTest.java b/core/azure-core-amqp/src/test/java/com/azure/core/amqp/RetryTest.java
new file mode 100644
index 000000000000..fa30f6d5cce2
--- /dev/null
+++ b/core/azure-core-amqp/src/test/java/com/azure/core/amqp/RetryTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp;
+
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorCondition;
+import com.azure.core.amqp.exception.ErrorContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.Duration;
+
+public class RetryTest {
+ private final ErrorContext errorContext = new ErrorContext("test-namespace");
+
+ /**
+ * Verifies that when the service is busy and we retry an exception multiple times, the retry duration gets longer.
+ */
+ @Test
+ public void defaultRetryPolicy() {
+ // Arrange
+ final Retry retry = Retry.getDefaultRetry();
+ final AmqpException exception = new AmqpException(true, ErrorCondition.SERVER_BUSY_ERROR, "error message", errorContext);
+ final Duration remainingTime = Duration.ofSeconds(60);
+
+ // Act
+ retry.incrementRetryCount();
+ final Duration firstRetryInterval = retry.getNextRetryInterval(exception, remainingTime);
+ Assert.assertNotNull(firstRetryInterval);
+
+ retry.incrementRetryCount();
+ final Duration leftoverTime = remainingTime.minus(firstRetryInterval);
+ final Duration secondRetryInterval = retry.getNextRetryInterval(exception, leftoverTime);
+
+ // Assert
+ Assert.assertNotNull(secondRetryInterval);
+ Assert.assertTrue(secondRetryInterval.toNanos() > firstRetryInterval.toNanos());
+ }
+
+ /**
+ * Verifies we can increment the retry count.
+ */
+ @Test
+ public void canIncrementRetryCount() {
+ Retry retry = Retry.getDefaultRetry();
+ Assert.assertEquals(0, retry.getRetryCount());
+ Assert.assertEquals(0, retry.incrementRetryCount());
+
+ Assert.assertEquals(1, retry.getRetryCount());
+ Assert.assertEquals(1, retry.incrementRetryCount());
+
+ Assert.assertEquals(2, retry.getRetryCount());
+ Assert.assertEquals(2, retry.incrementRetryCount());
+
+ retry.resetRetryInterval();
+
+ Assert.assertEquals(0, retry.getRetryCount());
+ Assert.assertEquals(0, retry.incrementRetryCount());
+
+ Assert.assertEquals(1, retry.getRetryCount());
+ }
+
+ @Test
+ public void isRetriableException() {
+ final Exception exception = new AmqpException(true, "error message", errorContext);
+ Assert.assertTrue(Retry.isRetriableException(exception));
+ }
+
+ @Test
+ public void notRetriableException() {
+ final Exception invalidException = new RuntimeException("invalid exception");
+ Assert.assertFalse(Retry.isRetriableException(invalidException));
+ }
+
+ @Test
+ public void notRetriableExceptionNotTransient() {
+ final Exception invalidException = new AmqpException(false, "Some test exception", errorContext);
+ Assert.assertFalse(Retry.isRetriableException(invalidException));
+ }
+
+ /**
+ * Verifies that using no retry policy does not allow us to retry a failed request.
+ */
+ @Test
+ public void noRetryPolicy() {
+ // Arrange
+ final Retry noRetry = Retry.getNoRetry();
+ final Exception exception = new AmqpException(true, "error message", errorContext);
+ final Duration remainingTime = Duration.ofSeconds(60);
+
+ // Act
+ final Duration nextRetryInterval = noRetry.getNextRetryInterval(exception, remainingTime);
+ int retryCount = noRetry.incrementRetryCount();
+
+ // Assert
+ Assert.assertEquals(0, retryCount);
+ Assert.assertNull(nextRetryInterval);
+ }
+
+ /**
+ * Verifies that if we exceed the number of allowed retry attempts, the next retry interval, even if there is time
+ * remaining, is null.
+ */
+ @Test
+ public void excessMaxRetry() {
+ // Arrange
+ final Retry retry = Retry.getDefaultRetry();
+ final Exception exception = new AmqpException(true, "error message", errorContext);
+ final Duration sixtySec = Duration.ofSeconds(60);
+
+ // Simulates that we've tried to retry the max number of requests this allows.
+ for (int i = 0; i < retry.getMaxRetryCount(); i++) {
+ retry.incrementRetryCount();
+ }
+
+ // Act
+ final Duration nextRetryInterval = retry.getNextRetryInterval(exception, sixtySec);
+
+ // Assert
+ Assert.assertEquals(Retry.DEFAULT_MAX_RETRY_COUNT, retry.getRetryCount());
+ Assert.assertNull(nextRetryInterval);
+ }
+}
diff --git a/core/azure-core-amqp/src/test/java/com/azure/core/amqp/exception/OperationCancelledExceptionTest.java b/core/azure-core-amqp/src/test/java/com/azure/core/amqp/exception/OperationCancelledExceptionTest.java
new file mode 100644
index 000000000000..fb77190d9ea6
--- /dev/null
+++ b/core/azure-core-amqp/src/test/java/com/azure/core/amqp/exception/OperationCancelledExceptionTest.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.amqp.exception;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OperationCancelledExceptionTest {
+ @Test
+ public void correctMessage() {
+ final String message = "A test message.";
+ final OperationCancelledException exception = new OperationCancelledException(message, null);
+
+ Assert.assertEquals(message, exception.getMessage());
+ }
+
+ @Test
+ public void correctMessageAndThrowable() {
+ // Arrange
+ final String message = "A test message.";
+ final Throwable innerException = new IllegalArgumentException("An argument");
+
+ // Act
+ final OperationCancelledException exception = new OperationCancelledException(message, innerException, null);
+
+ // Arrange
+ Assert.assertEquals(message, exception.getMessage());
+ Assert.assertEquals(innerException, exception.getCause());
+ }
+}
diff --git a/core/azure-core/src/main/java/com/azure/core/implementation/util/ImplUtils.java b/core/azure-core/src/main/java/com/azure/core/implementation/util/ImplUtils.java
index 8049c040d0cd..272b93e2d0db 100644
--- a/core/azure-core/src/main/java/com/azure/core/implementation/util/ImplUtils.java
+++ b/core/azure-core/src/main/java/com/azure/core/implementation/util/ImplUtils.java
@@ -94,6 +94,15 @@ public static boolean isNullOrEmpty(Map map) {
return map == null || map.isEmpty();
}
+ /*
+ * Checks if the character sequence is null or empty.
+ * @param charSequence Character sequence being checked for nullness or emptiness.
+ * @return True if the character sequence is null or empty, false otherwise.
+ */
+ public static boolean isNullOrEmpty(CharSequence charSequence) {
+ return charSequence == null || charSequence.length() == 0;
+ }
+
/*
* Turns an array into a string mapping each element to a string and delimits them using a coma.
* @param array Array being formatted to a string.
@@ -130,15 +139,6 @@ public static T findFirstOfType(Object[] args, Class clazz) {
return null;
}
- /*
- * Checks if the character sequence is null or empty.
- * @param charSequence Character sequence being checked for nullness or emptiness.
- * @return True if the character sequence is null or empty, false otherwise.
- */
- public static boolean isNullOrEmpty(CharSequence charSequence) {
- return charSequence == null || charSequence.length() == 0;
- }
-
/*
* Extracts and combines the generic items from all the pages linked together.
* @param page The paged response from server holding generic items.
diff --git a/core/pom.xml b/core/pom.xml
index 13e4172c9c6d..a5286d01f928 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -54,6 +54,7 @@
azure-core
+ azure-core-amqpazure-core-authazure-core-managementazure-core-test
diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml
index 55614025ffa7..a32bd442783d 100644
--- a/eng/jacoco-test-coverage/pom.xml
+++ b/eng/jacoco-test-coverage/pom.xml
@@ -41,6 +41,11 @@
azure-core${version}
+
+ com.azure
+ azure-core-amqp
+ ${azure-core-amqp.version}
+ com.azureazure-core-auth
@@ -66,6 +71,11 @@
azure-keyvault-secrets${version}
+
+ com.azure
+ azure-messaging-eventhubs
+ ${version}
+ com.azuretracing-opentelemetry
diff --git a/eng/spotbugs-aggregate-report/pom.xml b/eng/spotbugs-aggregate-report/pom.xml
index 4fdbbcb3c357..ae37e1cb9744 100644
--- a/eng/spotbugs-aggregate-report/pom.xml
+++ b/eng/spotbugs-aggregate-report/pom.xml
@@ -20,6 +20,7 @@
2.0.010.5.01.0.0-SNAPSHOT
+ 1.0.0-SNAPSHOT
@@ -46,9 +47,11 @@
..\..\appconfiguration\client\src\samples\java..\..\core\azure-core\src\main\java..\..\core\azure-core\src\samples\java
+ ..\..\core\azure-core-amqp\src\main\java..\..\core\azure-core-auth\src\main\java..\..\core\azure-core-management\src\main\java..\..\core\azure-core-test\src\main\java
+ ..\..\eventhubs\client\src\main\java
@@ -113,6 +116,11 @@
azure-core${azure-core.version}
+
+ com.azure
+ azure-core-amqp
+ ${azure-core-amqp.version}
+ com.azureazure-core-auth
@@ -138,6 +146,11 @@
azure-data-appconfiguration${azure-data-appconfiguration.version}
+
+ com.azure
+ azure-messaging-eventhubs
+ ${azure-messaging-eventhubs.version}
+ generate-sources
diff --git a/eventhubs/client/azure-eventhubs/pom.xml b/eventhubs/client/azure-eventhubs/pom.xml
new file mode 100644
index 000000000000..d84cd4f65d10
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ com.azure
+ azure-messaging-eventhubs-parent
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ com.azure
+ azure-messaging-eventhubs
+ 1.0.0-SNAPSHOT
+
+ Microsoft Azure client library for Event Hubs
+ Libraries built on Microsoft Azure Event Hubs
+ https://github.com/Azure/azure-sdk-for-java
+
+
+
+ azure-java-build-docs
+ ${site.url}/site/${project.artifactId}
+
+
+
+
+ scm:git:https://github.com/Azure/azure-sdk-for-java
+
+
+
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventData.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventData.java
new file mode 100644
index 000000000000..00a38e5a0fd7
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventData.java
@@ -0,0 +1,333 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.MessageConstant;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.Data;
+import org.apache.qpid.proton.amqp.messaging.Section;
+import org.apache.qpid.proton.message.Message;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.azure.core.amqp.MessageConstant.ENQUEUED_TIME_UTC_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.OFFSET_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.PARTITION_KEY_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.PUBLISHER_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.SEQUENCE_NUMBER_ANNOTATION_NAME;
+
+/**
+ * The data structure encapsulating the event being sent-to and received-from Event Hubs. Each Event Hub partition can
+ * be visualized as a stream of {@link EventData}.
+ *
+ *
+ * Here's how AMQP message sections map to {@link EventData}. For reference, the specification can be found here:
+ * AMQP 1.0 specification
+ *
+ *
{@link #body()} - if AMQPMessage.Body has Data section
+ *
+ *
+ *
+ * Serializing a received {@link EventData} with AMQP sections other than ApplicationProperties (with primitive Java
+ * types) and Data section is not supported.
+ *
+ */
+public class EventData implements Comparable {
+ /*
+ * These are properties owned by the service and set when a message is received.
+ */
+ public static final Set RESERVED_SYSTEM_PROPERTIES;
+
+ private final Map properties;
+ private final ByteBuffer body;
+ private final SystemProperties systemProperties;
+
+ static {
+ final Set properties = new HashSet<>();
+ properties.add(OFFSET_ANNOTATION_NAME.getValue());
+ properties.add(PARTITION_KEY_ANNOTATION_NAME.getValue());
+ properties.add(SEQUENCE_NUMBER_ANNOTATION_NAME.getValue());
+ properties.add(ENQUEUED_TIME_UTC_ANNOTATION_NAME.getValue());
+ properties.add(PUBLISHER_ANNOTATION_NAME.getValue());
+
+ RESERVED_SYSTEM_PROPERTIES = Collections.unmodifiableSet(properties);
+ }
+
+ /**
+ * Creates an event containing the {@code data}.
+ *
+ * @param body The data to set for this event.
+ */
+ public EventData(byte[] body) {
+ this(ByteBuffer.wrap(body));
+ }
+
+ /**
+ * Creates an event containing the {@code body}.
+ *
+ * @param body The data to set for this event.
+ * @throws NullPointerException if {@code body} is {@code null}.
+ */
+ public EventData(ByteBuffer body) {
+ Objects.requireNonNull(body);
+
+ this.body = body;
+ this.properties = new HashMap<>();
+ this.systemProperties = new SystemProperties(Collections.emptyMap());
+ }
+
+ /*
+ * Creates an event from a message
+ */
+ EventData(Message message) {
+ if (message == null) {
+ throw new IllegalArgumentException("'message' cannot be null");
+ }
+
+ final Map messageAnnotations = message.getMessageAnnotations().getValue();
+ final HashMap receiveProperties = new HashMap<>();
+
+ for (Map.Entry annotation : messageAnnotations.entrySet()) {
+ receiveProperties.put(annotation.getKey().toString(), annotation.getValue());
+ }
+
+ if (message.getProperties() != null) {
+ addMapEntry(receiveProperties, MessageConstant.MESSAGE_ID, message.getMessageId());
+ addMapEntry(receiveProperties, MessageConstant.USER_ID, message.getUserId());
+ addMapEntry(receiveProperties, MessageConstant.TO, message.getAddress());
+ addMapEntry(receiveProperties, MessageConstant.SUBJECT, message.getSubject());
+ addMapEntry(receiveProperties, MessageConstant.REPLY_TO, message.getReplyTo());
+ addMapEntry(receiveProperties, MessageConstant.CORRELATION_ID, message.getCorrelationId());
+ addMapEntry(receiveProperties, MessageConstant.CONTENT_TYPE, message.getContentType());
+ addMapEntry(receiveProperties, MessageConstant.CONTENT_ENCODING, message.getContentEncoding());
+ addMapEntry(receiveProperties, MessageConstant.ABSOLUTE_EXPRITY_TIME, message.getExpiryTime());
+ addMapEntry(receiveProperties, MessageConstant.CREATION_TIME, message.getCreationTime());
+ addMapEntry(receiveProperties, MessageConstant.GROUP_ID, message.getGroupId());
+ addMapEntry(receiveProperties, MessageConstant.GROUP_SEQUENCE, message.getGroupSequence());
+ addMapEntry(receiveProperties, MessageConstant.REPLY_TO_GROUP_ID, message.getReplyToGroupId());
+ }
+
+ this.systemProperties = new SystemProperties(receiveProperties);
+ this.properties = message.getApplicationProperties() == null
+ ? new HashMap<>()
+ : message.getApplicationProperties().getValue();
+
+ final Section bodySection = message.getBody();
+ if (bodySection instanceof Data) {
+ Data bodyData = (Data) bodySection;
+ this.body = bodyData.getValue().asByteBuffer();
+ } else {
+ this.body = null;
+ }
+
+ message.clear();
+ }
+
+ /**
+ * Adds an application property associated with this event. If the {@code key} exists in the map, its existing value
+ * is overwritten.
+ *
+ * @param key The key for this application property
+ * @param value The value for this application property.
+ * @return The updated EventData object.
+ * @throws NullPointerException if {@code key} or {@code value} is null.
+ */
+ public EventData addProperty(String key, Object value) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(value);
+
+ properties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Gets the application property bag
+ *
+ * @return Application properties associated with this EventData.
+ */
+ public Map properties() {
+ return properties;
+ }
+
+ /**
+ * Properties that are populated by EventHubService. As these are populated by Service, they are only present on a
+ * received EventData.
+ *
+ * @return an encapsulation of all SystemProperties appended by EventHubs service into EventData. {@code null} if
+ * the {@link EventData} is not received and is created by the public constructors.
+ */
+ public Map systemProperties() {
+ return systemProperties;
+ }
+
+ /**
+ * Gets the actual payload/data wrapped by EventData.
+ *
+ * @return ByteBuffer representing the data.
+ */
+ public ByteBuffer body() {
+ return body;
+ }
+
+ /**
+ * Gets the offset of the event when it was received from the associated Event Hub partition.
+ *
+ * @return The offset within the Event Hub partition.
+ */
+ public String offset() {
+ return systemProperties.offset();
+ }
+
+ /**
+ * Gets a partition key used for message partitioning. If it exists, this value was used to compute a hash to
+ * select a partition to send the message to.
+ *
+ * @return A partition key for this Event Data.
+ */
+ public String partitionKey() {
+ return systemProperties.partitionKey();
+ }
+
+ /**
+ * Gets the instant, in UTC, of when the event was enqueued in the Event Hub partition.
+ *
+ * @return The instant, in UTC, this was enqueued in the Event Hub partition.
+ */
+ public Instant enqueuedTime() {
+ return systemProperties.enqueuedTime();
+ }
+
+ /**
+ * Gets the sequence number assigned to the event when it was enqueued in the associated Event Hub partition. This
+ * is unique for every message received in the Event Hub partition.
+ *
+ * @return Sequence number for this event.
+ * @throws IllegalStateException if {@link #systemProperties()} does not contain the sequence number in a retrieved
+ * event.
+ */
+ public long sequenceNumber() {
+ return systemProperties.sequenceNumber();
+ }
+
+ private void addMapEntry(Map map, MessageConstant key, Object content) {
+ if (content == null) {
+ return;
+ }
+
+ map.put(key.getValue(), content);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int compareTo(EventData other) {
+ return Long.compare(
+ this.sequenceNumber(),
+ other.sequenceNumber()
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ EventData eventData = (EventData) o;
+ return Objects.equals(body, eventData.body);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(body);
+ }
+
+ /**
+ * A collection of properties populated by Azure Event Hubs service.
+ */
+ private static class SystemProperties extends HashMap {
+ private static final long serialVersionUID = -2827050124966993723L;
+
+ SystemProperties(final Map map) {
+ super(Collections.unmodifiableMap(map));
+ }
+
+ /**
+ * Gets the offset within the Event Hubs stream.
+ *
+ * @return The offset within the Event Hubs stream.
+ */
+ private String offset() {
+ return this.getSystemProperty(OFFSET_ANNOTATION_NAME.getValue());
+ }
+
+ /**
+ * Gets a partition key used for message partitioning. If it exists, this value was used to compute a hash to
+ * select a partition to send the message to.
+ *
+ * @return A partition key for this Event Data.
+ */
+ private String partitionKey() {
+ return this.getSystemProperty(PARTITION_KEY_ANNOTATION_NAME.getValue());
+ }
+
+ /**
+ * Gets the time this event was enqueued in the Event Hub.
+ *
+ * @return The time this was enqueued in the service.
+ */
+ private Instant enqueuedTime() {
+ final Date enqueuedTimeValue = this.getSystemProperty(ENQUEUED_TIME_UTC_ANNOTATION_NAME.getValue());
+ return enqueuedTimeValue != null ? enqueuedTimeValue.toInstant() : null;
+ }
+
+ /**
+ * Gets the sequence number in the event stream for this event. This is unique for every message received in the
+ * Event Hub.
+ *
+ * @return Sequence number for this event.
+ * @throws IllegalStateException if {@link SystemProperties} does not contain the sequence number in a retrieved
+ * event.
+ */
+ private long sequenceNumber() {
+ final Long sequenceNumber = this.getSystemProperty(SEQUENCE_NUMBER_ANNOTATION_NAME.getValue());
+
+ if (sequenceNumber == null) {
+ throw new IllegalStateException(String.format(Locale.US, "sequenceNumber: %s should always be in map.", SEQUENCE_NUMBER_ANNOTATION_NAME.getValue()));
+ }
+
+ return sequenceNumber;
+ }
+
+ @SuppressWarnings("unchecked")
+ private T getSystemProperty(final String key) {
+ if (this.containsKey(key)) {
+ return (T) (this.get(key));
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventDataBatch.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventDataBatch.java
new file mode 100644
index 000000000000..f305a5edc904
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventDataBatch.java
@@ -0,0 +1,188 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.MessageConstant;
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorCondition;
+import com.azure.messaging.eventhubs.implementation.AmqpConstants;
+import com.azure.messaging.eventhubs.implementation.ErrorContextProvider;
+import org.apache.qpid.proton.Proton;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
+import org.apache.qpid.proton.amqp.messaging.Data;
+import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
+import org.apache.qpid.proton.message.Message;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/*
+ * A class for aggregating EventData into a single, size-limited, batch that will be treated as a single message when
+ * sent to the Azure Event Hubs service.
+ */
+final class EventDataBatch {
+ private final int maxMessageSize;
+ private final String partitionKey;
+ private final ErrorContextProvider contextProvider;
+ private final List events;
+ private final byte[] eventBytes;
+ private int currentSize = 0;
+
+ EventDataBatch(int maxMessageSize, String partitionKey, ErrorContextProvider contextProvider) {
+ this.maxMessageSize = maxMessageSize;
+ this.partitionKey = partitionKey;
+ this.contextProvider = contextProvider;
+ this.events = new LinkedList<>();
+ this.currentSize = (maxMessageSize / 65536) * 1024; // reserve 1KB for every 64KB
+ this.eventBytes = new byte[maxMessageSize];
+ }
+
+ int getSize() {
+ return events.size();
+ }
+
+ boolean tryAdd(final EventData eventData) {
+
+ if (eventData == null) {
+ throw new IllegalArgumentException("eventData cannot be null");
+ }
+
+ final int size;
+ try {
+ size = getSize(eventData, events.isEmpty());
+ } catch (java.nio.BufferOverflowException exception) {
+ throw new AmqpException(false, ErrorCondition.LINK_PAYLOAD_SIZE_EXCEEDED,
+ String.format(Locale.US, "Size of the payload exceeded Maximum message size: %s kb", maxMessageSize / 1024),
+ contextProvider.getErrorContext());
+ }
+
+ if (this.currentSize + size > this.maxMessageSize) {
+ return false;
+ }
+
+ this.events.add(eventData);
+ this.currentSize += size;
+ return true;
+ }
+
+ List getEvents() {
+ return events;
+ }
+
+ String getPartitionKey() {
+ return this.partitionKey;
+ }
+
+ private int getSize(final EventData eventData, final boolean isFirst) {
+ Objects.requireNonNull(eventData);
+
+ final Message amqpMessage = createAmqpMessage(eventData, partitionKey);
+ int eventSize = amqpMessage.encode(this.eventBytes, 0, maxMessageSize); // actual encoded bytes size
+ eventSize += 16; // data section overhead
+
+ if (isFirst) {
+ amqpMessage.setBody(null);
+ amqpMessage.setApplicationProperties(null);
+ amqpMessage.setProperties(null);
+ amqpMessage.setDeliveryAnnotations(null);
+
+ eventSize += amqpMessage.encode(this.eventBytes, 0, maxMessageSize);
+ }
+
+ return eventSize;
+ }
+
+ /*
+ * Creates the AMQP message represented by the event data
+ */
+ private static Message createAmqpMessage(EventData event, String partitionKey) {
+ final Message message = Proton.message();
+
+ if (event.properties() != null && !event.properties().isEmpty()) {
+ final ApplicationProperties applicationProperties = new ApplicationProperties(event.properties());
+ message.setApplicationProperties(applicationProperties);
+ }
+
+ if (event.systemProperties() != null) {
+ event.systemProperties().forEach((key, value) -> {
+ if (EventData.RESERVED_SYSTEM_PROPERTIES.contains(key)) {
+ return;
+ }
+
+ final MessageConstant constant = MessageConstant.fromString(key);
+
+ if (constant != null) {
+ switch (constant) {
+ case MESSAGE_ID:
+ message.setMessageId(value);
+ break;
+ case USER_ID:
+ message.setUserId((byte[]) value);
+ break;
+ case TO:
+ message.setAddress((String) value);
+ break;
+ case SUBJECT:
+ message.setSubject((String) value);
+ break;
+ case REPLY_TO:
+ message.setReplyTo((String) value);
+ break;
+ case CORRELATION_ID:
+ message.setCorrelationId(value);
+ break;
+ case CONTENT_TYPE:
+ message.setContentType((String) value);
+ break;
+ case CONTENT_ENCODING:
+ message.setContentEncoding((String) value);
+ break;
+ case ABSOLUTE_EXPRITY_TIME:
+ message.setExpiryTime((long) value);
+ break;
+ case CREATION_TIME:
+ message.setCreationTime((long) value);
+ break;
+ case GROUP_ID:
+ message.setGroupId((String) value);
+ break;
+ case GROUP_SEQUENCE:
+ message.setGroupSequence((long) value);
+ break;
+ case REPLY_TO_GROUP_ID:
+ message.setReplyToGroupId((String) value);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format(Locale.US, "Property is not a recognized reserved property name: %s", key));
+ }
+ } else {
+ final MessageAnnotations messageAnnotations = (message.getMessageAnnotations() == null)
+ ? new MessageAnnotations(new HashMap<>())
+ : message.getMessageAnnotations();
+ messageAnnotations.getValue().put(Symbol.getSymbol(key), value);
+ message.setMessageAnnotations(messageAnnotations);
+ }
+ });
+ }
+
+ if (partitionKey != null) {
+ final MessageAnnotations messageAnnotations = (message.getMessageAnnotations() == null)
+ ? new MessageAnnotations(new HashMap<>())
+ : message.getMessageAnnotations();
+ messageAnnotations.getValue().put(AmqpConstants.PARTITION_KEY, partitionKey);
+ message.setMessageAnnotations(messageAnnotations);
+ }
+
+ if (event.body() != null) {
+ message.setBody(new Data(Binary.create(event.body())));
+ }
+
+ return message;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClient.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClient.java
new file mode 100644
index 000000000000..fd82b9f207e1
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClient.java
@@ -0,0 +1,284 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.AmqpConnection;
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorContext;
+import com.azure.core.implementation.util.ImplUtils;
+import com.azure.messaging.eventhubs.implementation.AmqpReceiveLink;
+import com.azure.messaging.eventhubs.implementation.AmqpResponseMapper;
+import com.azure.messaging.eventhubs.implementation.AmqpSendLink;
+import com.azure.messaging.eventhubs.implementation.ConnectionOptions;
+import com.azure.messaging.eventhubs.implementation.EventHubConnection;
+import com.azure.messaging.eventhubs.implementation.EventHubManagementNode;
+import com.azure.messaging.eventhubs.implementation.EventHubSession;
+import com.azure.messaging.eventhubs.implementation.ManagementChannel;
+import com.azure.messaging.eventhubs.implementation.ReactorConnection;
+import com.azure.messaging.eventhubs.implementation.ReactorHandlerProvider;
+import com.azure.messaging.eventhubs.implementation.ReactorProvider;
+import com.azure.messaging.eventhubs.implementation.StringUtil;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The main point of interaction with Azure Event Hubs, the client offers a connection to a specific Event Hub within
+ * the Event Hubs namespace and offers operations for sending event data, receiving events, and inspecting the connected
+ * Event Hub.
+ */
+public class EventHubClient implements Closeable {
+ /**
+ * The name of the default consumer group in the Event Hubs service.
+ */
+ public static final String DEFAULT_CONSUMER_GROUP_NAME = "$Default";
+
+ private static final String RECEIVER_ENTITY_PATH_FORMAT = "%s/ConsumerGroups/%s/Partitions/%s";
+ private static final String SENDER_ENTITY_PATH_FORMAT = "%s/Partitions/%s";
+
+ private final String connectionId;
+ private final Mono connectionMono;
+ private final AtomicBoolean hasConnection = new AtomicBoolean(false);
+ private final ConnectionOptions connectionOptions;
+ private final String eventHubPath;
+ private final EventHubProducerOptions defaultProducerOptions;
+ private final EventHubConsumerOptions defaultConsumerOptions;
+
+ EventHubClient(ConnectionOptions connectionOptions, ReactorProvider provider, ReactorHandlerProvider handlerProvider) {
+ Objects.requireNonNull(connectionOptions);
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(handlerProvider);
+
+ this.connectionOptions = connectionOptions;
+ this.eventHubPath = connectionOptions.eventHubPath();
+ this.connectionId = StringUtil.getRandomString("MF");
+ this.connectionMono = Mono.fromCallable(() -> {
+ return (EventHubConnection) new ReactorConnection(connectionId, connectionOptions, provider, handlerProvider, new ResponseMapper());
+ }).doOnSubscribe(c -> hasConnection.set(true))
+ .cache();
+
+ this.defaultProducerOptions = new EventHubProducerOptions()
+ .retry(connectionOptions.retryPolicy())
+ .timeout(connectionOptions.timeout());
+ this.defaultConsumerOptions = new EventHubConsumerOptions()
+ .retry(connectionOptions.retryPolicy())
+ .scheduler(connectionOptions.scheduler());
+ }
+
+ /**
+ * Retrieves information about an Event Hub, including the number of partitions present and their identifiers.
+ *
+ * @return The set of information for the Event Hub that this client is associated with.
+ */
+ public Mono getProperties() {
+ return connectionMono.flatMap(connection -> connection.getManagementNode().flatMap(EventHubManagementNode::getEventHubProperties));
+ }
+
+ /**
+ * Retrieves the identifiers for the partitions of an Event Hub.
+ *
+ * @return A Flux of identifiers for the partitions of an Event Hub.
+ */
+ public Flux getPartitionIds() {
+ return getProperties().flatMapMany(properties -> Flux.fromArray(properties.partitionIds()));
+ }
+
+ /**
+ * Retrieves information about a specific partition for an Event Hub, including elements that describe the available
+ * events in the partition event stream.
+ *
+ * @param partitionId The unique identifier of a partition associated with the Event Hub.
+ * @return The set of information for the requested partition under the Event Hub this client is associated with.
+ */
+ public Mono getPartitionProperties(String partitionId) {
+ return connectionMono.flatMap(
+ connection -> connection.getManagementNode().flatMap(node -> {
+ return node.getPartitionProperties(partitionId);
+ }));
+ }
+
+ /**
+ * Creates an Event Hub producer responsible for transmitting {@link EventData} to the Event Hub, grouped together
+ * in batches. Event data is automatically routed to an available partition.
+ *
+ * @return A new {@link EventHubProducer}.
+ */
+ public EventHubProducer createProducer() {
+ return createProducer(defaultProducerOptions);
+ }
+
+ /**
+ * Creates an Event Hub producer responsible for transmitting {@link EventData} to the Event Hub, grouped together
+ * in batches. If {@link EventHubProducerOptions#partitionId() options.partitionId()} is not {@code null}, the
+ * events are routed to that specific partition. Otherwise, events are automatically routed to an available
+ * partition.
+ *
+ * @param options The set of options to apply when creating the producer.
+ * @return A new {@link EventHubProducer}.
+ * @throws NullPointerException if {@code options} is {@code null}.
+ */
+ public EventHubProducer createProducer(EventHubProducerOptions options) {
+ Objects.requireNonNull(options);
+
+ final EventHubProducerOptions clonedOptions = options.clone();
+ if (clonedOptions.timeout() == null) {
+ clonedOptions.timeout(connectionOptions.timeout());
+ }
+ if (clonedOptions.retry() == null) {
+ clonedOptions.retry(connectionOptions.retryPolicy());
+ }
+
+ final String entityPath;
+ final String linkName;
+
+ if (ImplUtils.isNullOrEmpty(options.partitionId())) {
+ entityPath = eventHubPath;
+ linkName = StringUtil.getRandomString("EC");
+ } else {
+ entityPath = String.format(Locale.US, SENDER_ENTITY_PATH_FORMAT, eventHubPath, options.partitionId());
+ linkName = StringUtil.getRandomString("PS");
+ }
+
+ final Mono amqpLinkMono = connectionMono.flatMap(connection -> connection.createSession(entityPath)
+ .flatMap(session -> session.createProducer(linkName, entityPath, clonedOptions.timeout(), clonedOptions.retry())
+ .cast(AmqpSendLink.class)))
+ .publish(x -> x);
+
+ return new EventHubProducer(amqpLinkMono, clonedOptions);
+ }
+
+ /**
+ * Creates an Event Hub consumer responsible for reading {@link EventData} from a specific Event Hub partition,
+ * as a member of the specified consumer group, and begins reading events from the {@code eventPosition}.
+ *
+ * The consumer created is non-exclusive, allowing multiple consumers from the same consumer group to be actively
+ * reading events from the partition. These non-exclusive consumers are sometimes referred to as "Non-epoch
+ * Consumers".
+ *
+ * @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"}.
+ * @param partitionId The identifier of the Event Hub partition.
+ * @param eventPosition The position within the partition where the consumer should begin reading events.
+ * @return A new {@link EventHubConsumer} that receives events from the partition at the given position.
+ * @throws NullPointerException If {@code eventPosition}, or {@code options} is {@code null}.
+ * @throws IllegalArgumentException If {@code consumerGroup} or {@code partitionId} is {@code null} or an empty
+ * string.
+ */
+ public EventHubConsumer createConsumer(String consumerGroup, String partitionId, EventPosition eventPosition) {
+ return createConsumer(consumerGroup, partitionId, eventPosition, defaultConsumerOptions);
+ }
+
+ /**
+ * Creates an Event Hub consumer responsible for reading {@link EventData} from a specific Event Hub partition,
+ * as a member of the configured consumer group, and begins reading events from the specified {@code eventPosition}.
+ *
+ *
+ * A consumer may be exclusive, which asserts ownership over the partition for the consumer group to ensure that
+ * only one consumer from that group is reading the from the partition. These exclusive consumers are sometimes
+ * referred to as "Epoch Consumers."
+ *
+ * A consumer may also be non-exclusive, allowing multiple consumers from the same consumer group to be actively
+ * reading events from the partition. These non-exclusive consumers are sometimes referred to as "Non-epoch
+ * Consumers."
+ *
+ * Designating a consumer as exclusive may be specified in the {@code options}, by setting
+ * {@link EventHubConsumerOptions#ownerLevel(Long)} to a non-null value. By default, consumers are
+ * created as non-exclusive.
+ *
+ *
+ * @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"}.
+ * @param partitionId The identifier of the Event Hub partition from which events will be received.
+ * @param eventPosition The position within the partition where the consumer should begin reading events.
+ * @param options The set of options to apply when creating the consumer.
+ * @return An new {@link EventHubConsumer} that receives events from the partition with all configured
+ * {@link EventHubConsumerOptions}.
+ * @throws NullPointerException If {@code eventPosition}, or {@code options} is {@code null}.
+ * @throws IllegalArgumentException If {@code consumerGroup} or {@code partitionId} is {@code null} or an empty
+ * string.
+ */
+ public EventHubConsumer createConsumer(String consumerGroup, String partitionId, EventPosition eventPosition,
+ EventHubConsumerOptions options) {
+ Objects.requireNonNull(eventPosition);
+ Objects.requireNonNull(options);
+
+ if (ImplUtils.isNullOrEmpty(consumerGroup)) {
+ throw new IllegalArgumentException("'consumerGroup' cannot be null or empty.");
+ }
+ if (ImplUtils.isNullOrEmpty(partitionId)) {
+ throw new IllegalArgumentException("'partitionId' cannot be null or empty.");
+ }
+
+ final EventHubConsumerOptions clonedOptions = options.clone();
+ if (clonedOptions.scheduler() == null) {
+ clonedOptions.scheduler(connectionOptions.scheduler());
+ }
+ if (clonedOptions.retry() == null) {
+ clonedOptions.retry(connectionOptions.retryPolicy());
+ }
+
+ final String linkName = StringUtil.getRandomString("PR");
+ final String entityPath = String.format(Locale.US, RECEIVER_ENTITY_PATH_FORMAT, eventHubPath, consumerGroup, partitionId);
+
+ final Mono receiveLinkMono = connectionMono.flatMap(connection -> connection.createSession(entityPath))
+ .cast(EventHubSession.class)
+ .flatMap(session -> {
+ return session.createConsumer(linkName, entityPath, eventPosition.getExpression(), connectionOptions.timeout(),
+ clonedOptions.retry(), options.ownerLevel(), options.identifier());
+ })
+ .cast(AmqpReceiveLink.class);
+
+ return new EventHubConsumer(receiveLinkMono, clonedOptions, connectionOptions.timeout());
+ }
+
+ /**
+ * Closes and disposes of connection to service. Any {@link EventHubConsumer EventHubConsumers} and
+ * {@link EventHubProducer EventHubProducers} created with this instance will have their connections closed.
+ */
+ @Override
+ public void close() {
+ if (hasConnection.getAndSet(false)) {
+ try {
+ final AmqpConnection connection = connectionMono.block(connectionOptions.timeout());
+ if (connection != null) {
+ connection.close();
+ }
+ } catch (IOException exception) {
+ throw new AmqpException(false, "Unable to close connection to service", exception,
+ new ErrorContext(connectionOptions.host()));
+ }
+ }
+ }
+
+ private static class ResponseMapper implements AmqpResponseMapper {
+ @Override
+ public EventHubProperties toEventHubProperties(Map, ?> amqpBody) {
+ return new EventHubProperties(
+ (String) amqpBody.get(ManagementChannel.MANAGEMENT_ENTITY_NAME_KEY),
+ ((Date) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_CREATED_AT)).toInstant(),
+ (String[]) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_PARTITION_IDS));
+ }
+
+ @Override
+ public PartitionProperties toPartitionProperties(Map, ?> amqpBody) {
+ return new PartitionProperties(
+ (String) amqpBody.get(ManagementChannel.MANAGEMENT_ENTITY_NAME_KEY),
+ (String) amqpBody.get(ManagementChannel.MANAGEMENT_PARTITION_NAME_KEY),
+ (Long) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_BEGIN_SEQUENCE_NUMBER),
+ (Long) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_LAST_ENQUEUED_SEQUENCE_NUMBER),
+ (String) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_LAST_ENQUEUED_OFFSET),
+ ((Date) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_LAST_ENQUEUED_TIME_UTC)).toInstant(),
+ (Boolean) amqpBody.get(ManagementChannel.MANAGEMENT_RESULT_PARTITION_IS_EMPTY));
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java
new file mode 100644
index 000000000000..61d5b94210ba
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java
@@ -0,0 +1,301 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.Retry;
+import com.azure.core.amqp.TransportType;
+import com.azure.core.credentials.TokenCredential;
+import com.azure.core.exception.AzureException;
+import com.azure.core.implementation.util.ImplUtils;
+import com.azure.core.util.configuration.BaseConfigurations;
+import com.azure.core.util.configuration.Configuration;
+import com.azure.core.util.configuration.ConfigurationManager;
+import com.azure.messaging.eventhubs.implementation.CBSAuthorizationType;
+import com.azure.messaging.eventhubs.implementation.ClientConstants;
+import com.azure.messaging.eventhubs.implementation.ConnectionOptions;
+import com.azure.messaging.eventhubs.implementation.ConnectionStringProperties;
+import com.azure.messaging.eventhubs.implementation.ReactorHandlerProvider;
+import com.azure.messaging.eventhubs.implementation.ReactorProvider;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Builder to create an {@link EventHubClient}.
+ */
+public class EventHubClientBuilder {
+
+ private static final String AZURE_EVENT_HUBS_CONNECTION_STRING = "AZURE_EVENT_HUBS_CONNECTION_STRING";
+
+ private TokenCredential credentials;
+ private Configuration configuration;
+ private Duration timeout;
+ private ProxyConfiguration proxyConfiguration;
+ private Retry retry;
+ private Scheduler scheduler;
+ private TransportType transport;
+ private String host;
+ private String eventHubPath;
+
+ /**
+ * Creates a new instance with the default transport {@link TransportType#AMQP}.
+ */
+ public EventHubClientBuilder() {
+ transport = TransportType.AMQP;
+ }
+
+ /**
+ * Sets the credential information given a connection string to the Event Hub instance.
+ *
+ *
+ * If the connection string is copied from the Event Hubs namespace, it will likely not contain the path to the
+ * desired Event Hub, which is needed. In this case, the path 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 path.
+ *
+ *
+ * @param connectionString The connection string to use for connecting to the Event Hub instance. It is expected
+ * that the Event Hub path and the shared access key properties are contained in this connection string.
+ * @return The updated EventHubClientBuilder object.
+ * @throws IllegalArgumentException if {@code connectionString} is null or empty. Or, the {@code connectionString}
+ * does not contain the "EntityPath" key, which is the name of the Event Hub instance.
+ * @throws AzureException If the shared access signature token credential could not be created using the connection
+ * string.
+ */
+ public EventHubClientBuilder connectionString(String connectionString) {
+ final ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
+ final TokenCredential tokenCredential;
+ try {
+ tokenCredential = new EventHubSharedAccessKeyCredential(properties.sharedAccessKeyName(),
+ properties.sharedAccessKey(), ClientConstants.TOKEN_VALIDITY);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ throw new AzureException("Could not create the EventHubSharedAccessKeyCredential.", e);
+ }
+
+ return credential(properties.endpoint().getHost(), properties.eventHubPath(), tokenCredential);
+ }
+
+ /**
+ * Sets the credential information given a connection string to the Event Hubs namespace and a path 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 path.
+ * @param eventHubPath The path of the specific Event Hub to connect the client to.
+ * @return The updated EventHubClientBuilder object.
+ * @throws IllegalArgumentException if {@code connectionString} or {@code eventHubPath} is null or empty. Or, if the
+ * {@code connectionString} contains the Event Hub path.
+ * @throws AzureException If the shared access signature token credential could not be created using the connection
+ * string.
+ */
+ public EventHubClientBuilder connectionString(String connectionString, String eventHubPath) {
+ if (ImplUtils.isNullOrEmpty(eventHubPath)) {
+ throw new IllegalArgumentException("'eventHubPath' cannot be null or empty");
+ }
+
+ final ConnectionStringProperties properties = new ConnectionStringProperties(connectionString);
+ final TokenCredential tokenCredential;
+ try {
+ tokenCredential = new EventHubSharedAccessKeyCredential(properties.sharedAccessKeyName(),
+ properties.sharedAccessKey(), ClientConstants.TOKEN_VALIDITY);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ throw new AzureException("Could not create the EventHubSharedAccessKeyCredential.", e);
+ }
+
+ if (!ImplUtils.isNullOrEmpty(properties.eventHubPath())) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "'connectionString' contains an Event Hub path [%s]. Please use the"
+ + " credentials(String connectionString) overload. Or supply a 'connectionString' without"
+ + " 'EntityPath' in it.", properties.eventHubPath()));
+ }
+
+ return credential(properties.endpoint().getHost(), eventHubPath, tokenCredential);
+ }
+
+ /**
+ * Sets the configuration store that is used during construction of the service client.
+ *
+ * The default configuration store is a clone of the {@link ConfigurationManager#getConfiguration() global
+ * configuration store}, use {@link Configuration#NONE} to bypass using configuration settings during construction.
+ *
+ * @param configuration The configuration store used to
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder configuration(Configuration configuration) {
+ this.configuration = configuration;
+ return this;
+ }
+
+ /**
+ * Sets the credential information for which Event Hub instance to connect to, and how to authorize against it.
+ *
+ * @param host The fully qualified host name for the Event Hubs namespace. This is likely to be similar to
+ * {@literal "{your-namespace}.servicebus.windows.net}".
+ * @param eventHubPath The path of the specific 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 EventHubClientBuilder object.
+ * @throws IllegalArgumentException if {@code host} or {@code eventHubPath} is null or empty.
+ * @throws NullPointerException if {@code credentials} is null.
+ */
+ public EventHubClientBuilder credential(String host, String eventHubPath, TokenCredential credential) {
+ if (ImplUtils.isNullOrEmpty(host)) {
+ throw new IllegalArgumentException("'host' cannot be null or empty");
+ }
+ if (ImplUtils.isNullOrEmpty(eventHubPath)) {
+ throw new IllegalArgumentException("'eventHubPath' cannot be null or empty.");
+ }
+
+ Objects.requireNonNull(credential);
+
+ this.host = host;
+ this.credentials = credential;
+ this.eventHubPath = eventHubPath;
+ return this;
+ }
+
+ /**
+ * Sets the proxy configuration for EventHubClient.
+ *
+ * @param proxyConfiguration The proxy configuration to use.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder proxyConfiguration(ProxyConfiguration proxyConfiguration) {
+ this.proxyConfiguration = proxyConfiguration;
+ return this;
+ }
+
+ /**
+ * Sets the scheduler for operations such as connecting to and receiving or sending data to Event Hubs. If none is
+ * specified, an elastic pool is used.
+ *
+ * @param scheduler The scheduler for operations such as connecting to and receiving or sending data to Event Hubs.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder scheduler(Scheduler scheduler) {
+ this.scheduler = scheduler;
+ return this;
+ }
+
+ /**
+ * Sets the transport type by which all the communication with Azure Event Hubs occurs.
+ * Default value is {@link TransportType#AMQP}.
+ *
+ * @param transport The transport type to use.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder transportType(TransportType transport) {
+ this.transport = transport;
+ return this;
+ }
+
+ /**
+ * Sets the timeout for each connection, link, and session.
+ *
+ * @param timeout Duration for timeout.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder timeout(Duration timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets the retry policy for EventHubClient.
+ *
+ * @param retry The retry policy to use.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubClientBuilder retry(Retry retry) {
+ this.retry = retry;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link EventHubClient} based on the configuration set in this builder.
+ * Use the default not null values if the Connection parameters are not provided.
+ *
+ * @return A new {@link EventHubClient} instance.
+ * @throws IllegalArgumentException if the credentials have not been set using either
+ * {@link #connectionString(String)} or {@link #credential(String, String, TokenCredential)}.
+ */
+ public EventHubClient build() {
+ configuration = configuration == null ? ConfigurationManager.getConfiguration().clone() : configuration;
+
+ if (credentials == null) {
+ final String connectionString = configuration.get(AZURE_EVENT_HUBS_CONNECTION_STRING);
+
+ if (ImplUtils.isNullOrEmpty(connectionString)) {
+ throw new IllegalArgumentException("Credentials have not been set using 'EventHubClientBuilder.credentials(String)'"
+ + "EventHubClientBuilder.credentials(String, String, TokenCredential). And the connection string is"
+ + "not set in the '" + AZURE_EVENT_HUBS_CONNECTION_STRING + "' environment variable.");
+ }
+
+ connectionString(connectionString);
+ }
+
+ if (timeout == null) {
+ timeout = ClientConstants.OPERATION_TIMEOUT;
+ }
+
+ if (retry == null) {
+ retry = Retry.getDefaultRetry();
+ }
+
+ if (proxyConfiguration == null) {
+ proxyConfiguration = constructDefaultProxyConfiguration(configuration);
+ }
+
+ if (scheduler == null) {
+ scheduler = Schedulers.elastic();
+ }
+
+ final ReactorProvider provider = new ReactorProvider();
+ final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider);
+ final CBSAuthorizationType authorizationType = credentials instanceof EventHubSharedAccessKeyCredential
+ ? CBSAuthorizationType.SHARED_ACCESS_SIGNATURE
+ : CBSAuthorizationType.JSON_WEB_TOKEN;
+ final ConnectionOptions parameters = new ConnectionOptions(host, eventHubPath, credentials,
+ authorizationType, timeout, transport, retry, proxyConfiguration, scheduler);
+
+ return new EventHubClient(parameters, provider, handlerProvider);
+ }
+
+ private ProxyConfiguration constructDefaultProxyConfiguration(Configuration configuration) {
+ ProxyAuthenticationType authentication = ProxyAuthenticationType.NONE;
+ if (proxyConfiguration != null) {
+ authentication = proxyConfiguration.authentication();
+ }
+
+ String proxyAddress = configuration.get(BaseConfigurations.HTTP_PROXY);
+ Proxy proxy = null;
+ if (proxyAddress != null) {
+ final String[] hostPort = proxyAddress.split(":");
+ if (hostPort.length < 2) {
+ throw new IllegalArgumentException("HTTP_PROXY cannot be parsed into a proxy");
+ }
+
+ final String host = hostPort[0];
+ final int port = Integer.parseInt(hostPort[1]);
+ proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
+ }
+
+ final String username = configuration.get(ProxyConfiguration.PROXY_USERNAME);
+ final String password = configuration.get(ProxyConfiguration.PROXY_PASSWORD);
+
+ return new ProxyConfiguration(authentication, proxy, username, password);
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumer.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumer.java
new file mode 100644
index 000000000000..63412c55ea16
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumer.java
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.messaging.eventhubs.implementation.AmqpReceiveLink;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+
+/**
+ * This is a logical representation of receiving from an Event Hub partition.
+ *
+ *
+ * A {@link EventHubConsumer#receive()} is tied to a Event Hub partitionId + consumer group combination.
+ *
+ *
+ *
If {@link EventHubConsumer} is created where {@link EventHubConsumerOptions#ownerLevel()} has a
+ * value, then Event Hubs service will guarantee only one active receiver exists per partitionId and consumer group
+ * combination. This is the recommended approach to create a {@link EventHubConsumer}.
+ *
Multiple consumers per partitionId and consumer group combination can be created by not setting
+ * {@link EventHubConsumerOptions#ownerLevel()} when creating receivers.
+ *
+ *
+ * @see EventHubClient#createConsumer(String, String, EventPosition)
+ * @see EventHubClient#createConsumer(String, String, EventPosition, EventHubConsumerOptions)
+ */
+public class EventHubConsumer implements Closeable {
+ private static final AtomicReferenceFieldUpdater RECEIVE_LINK_FIELD_UPDATER =
+ AtomicReferenceFieldUpdater.newUpdater(EventHubConsumer.class, AmqpReceiveLink.class, "receiveLink");
+
+ private final Mono receiveLinkMono;
+ private final Duration operationTimeout;
+ private final AtomicInteger creditsToRequest = new AtomicInteger(1);
+ private final AtomicBoolean isDisposed = new AtomicBoolean();
+ private final ClientLogger logger = new ClientLogger(EventHubConsumer.class);
+ private final EmitterProcessor emitterProcessor;
+ private final Flux messageFlux;
+
+ private volatile AmqpReceiveLink receiveLink;
+
+ EventHubConsumer(Mono receiveLinkMono, EventHubConsumerOptions options, Duration operationTimeout) {
+ this.receiveLinkMono = receiveLinkMono;
+ this.emitterProcessor = EmitterProcessor.create(options.prefetchCount(), false);
+ this.operationTimeout = operationTimeout;
+
+ // Caching the created link so we don't invoke another link creation.
+ this.messageFlux = receiveLinkMono.flatMapMany(link -> {
+ if (RECEIVE_LINK_FIELD_UPDATER.compareAndSet(this, null, link)) {
+ logger.asInfo().log("Created AMQP receive link. Initialising prefetch credit: {}", options.prefetchCount());
+ link.addCredits(options.prefetchCount());
+
+ link.setEmptyCreditListener(() -> {
+ if (emitterProcessor.hasDownstreams()) {
+ return creditsToRequest.get();
+ } else {
+ logger.asVerbose().log("Emitter has no downstream subscribers. Not adding credits.");
+ return 0;
+ }
+ });
+ }
+
+ return link.receive().map(EventData::new);
+ }).subscribeWith(emitterProcessor);
+
+ emitterProcessor.doOnSubscribe(subscription -> {
+ if (receiveLink.getCredits() == 0) {
+ logger.asInfo().log("Subscription received and there are no remaining credits on the link. Adding more.");
+ receiveLink.addCredits(creditsToRequest.get());
+ }
+ }).doOnRequest(request -> {
+ logger.asInfo().log("Back pressure requested. Old value: {}. New value: {}", creditsToRequest.get(), request);
+ creditsToRequest.set((int) request);
+ });
+ }
+
+ /**
+ * Disposes of the consumer by closing the underlying connection to the service.
+ *
+ * @throws IOException if the underlying transport and its resources could not be disposed.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!isDisposed.getAndSet(true)) {
+ final AmqpReceiveLink receiveLink = RECEIVE_LINK_FIELD_UPDATER.getAndSet(this, null);
+ if (receiveLink != null) {
+ receiveLink.close();
+ }
+
+ emitterProcessor.dispose();
+ }
+ }
+
+ /**
+ * Begin receiving events until there are no longer any subscribers, or the parent
+ * {@link EventHubClient#close() EventHubClient.close()} is called.
+ *
+ * @return A stream of events for this receiver.
+ */
+ public Flux receive() {
+ return messageFlux;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerOptions.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerOptions.java
new file mode 100644
index 000000000000..2e99fe896d28
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerOptions.java
@@ -0,0 +1,219 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.Retry;
+import com.azure.core.implementation.util.ImplUtils;
+import reactor.core.scheduler.Scheduler;
+
+import java.time.Duration;
+import java.util.Locale;
+import java.util.Optional;
+
+/**
+ * Options when receiving events from Event Hubs.
+ */
+public class EventHubConsumerOptions implements Cloneable {
+
+ /**
+ * The maximum length, in characters, for the identifier assigned to an {@link EventHubConsumer}.
+ */
+ public static final int MAXIMUM_IDENTIFIER_LENGTH = 64;
+ /**
+ * The minimum value allowed for the prefetch count of the consumer.
+ */
+ public static final int MINIMUM_PREFETCH_COUNT = 1;
+ /**
+ * The maximum value allowed for the prefetch count of the consumer.
+ */
+ public static final int MAXIMUM_PREFETCH_COUNT = 8000;
+
+ // Default number of events to fetch when creating the consumer.
+ static final int DEFAULT_PREFETCH_COUNT = 500;
+
+ private String identifier;
+ private Long ownerLevel;
+ private Retry retryPolicy;
+ private Scheduler scheduler;
+ private int prefetchCount;
+
+ /**
+ * Creates a new instance with the default prefetch amount.
+ */
+ public EventHubConsumerOptions() {
+ this.prefetchCount = DEFAULT_PREFETCH_COUNT;
+ }
+
+ /**
+ * Sets an optional text-based identifier label to assign to an event consumer.
+ *
+ * @param identifier The receiver name.
+ * @return The updated {@link EventHubConsumerOptions} object.
+ * @throws IllegalArgumentException if {@code identifier} is greater than {@link #MAXIMUM_IDENTIFIER_LENGTH}.
+ */
+ public EventHubConsumerOptions identifier(String identifier) {
+ if (!ImplUtils.isNullOrEmpty(identifier) && identifier.length() > MAXIMUM_IDENTIFIER_LENGTH) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "identifier length cannot exceed %s", MAXIMUM_IDENTIFIER_LENGTH));
+ }
+
+ this.identifier = identifier;
+ return this;
+ }
+
+
+ /**
+ * Sets the {@code ownerLevel} value on this consumer. When populated, the level indicates that a consumer is
+ * intended to be the only reader of events for the requested partition and an associated consumer group. To do so,
+ * this consumer will attempt to assert ownership over the partition; in the case where more than one exclusive
+ * consumer attempts to assert ownership for the same partition/consumer group pair, the one having a larger
+ * {@link EventHubConsumerOptions#ownerLevel()} value will "win".
+ *
+ *
+ * When an exclusive consumer is used, those consumers which are not exclusive or which have a lower priority will
+ * either not be allowed to be created, if they already exist, will encounter an exception during the next attempted
+ * operation.
+ *
+ *
+ * @param priority The priority associated with an exclusive consumer; for a non-exclusive consumer, this value
+ * should be {@code null}.
+ * @return The updated {@link EventHubConsumerOptions} object.
+ * @throws IllegalArgumentException if {@code priority} is not {@code null} and is less than 0.
+ */
+ public EventHubConsumerOptions ownerLevel(Long priority) {
+ if (priority != null && priority < 0) {
+ throw new IllegalArgumentException("'priority' cannot be a negative value. Please specify a zero or positive long value.");
+ }
+
+ this.ownerLevel = priority;
+ return this;
+ }
+
+ /**
+ * Sets the retry policy used to govern retry attempts for receiving events. If not specified, the retry policy
+ * configured on the associated {@link EventHubClient} is used.
+ *
+ * @param retry The retry policy to use when receiving events.
+ * @return The updated {@link EventHubConsumerOptions} object.
+ */
+ public EventHubConsumerOptions retry(Retry retry) {
+ this.retryPolicy = retry;
+ return this;
+ }
+
+ /**
+ * Sets the count used by the receiver to control the number of events this receiver 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 EventHubConsumerOptions} object.
+ * @throws IllegalArgumentException if {@code prefetchCount} is less than the {@link #MINIMUM_PREFETCH_COUNT} or
+ * greater than {@link #MAXIMUM_PREFETCH_COUNT}.
+ */
+ public EventHubConsumerOptions prefetchCount(int prefetchCount) {
+ if (prefetchCount < MINIMUM_PREFETCH_COUNT) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "PrefetchCount, '%s' has to be above %s", prefetchCount, MINIMUM_PREFETCH_COUNT));
+ }
+
+ if (prefetchCount > MAXIMUM_PREFETCH_COUNT) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "PrefetchCount, '%s', has to be below %s", prefetchCount, MAXIMUM_PREFETCH_COUNT));
+ }
+
+ this.prefetchCount = prefetchCount;
+ return this;
+ }
+
+ /**
+ * Sets the scheduler for receiving events from Event Hubs. If not specified, the scheduler configured with the
+ * associated {@link EventHubClient} is used.
+ *
+ * @param scheduler The scheduler for receiving events.
+ * @return The updated EventHubClientBuilder object.
+ */
+ public EventHubConsumerOptions scheduler(Scheduler scheduler) {
+ this.scheduler = scheduler;
+ return this;
+ }
+
+ /**
+ * Gets the optional text-based identifier label to assign to an event receiver.
+ * The identifier is used for informational purposes only. If not specified, the receiver will have no assigned
+ * identifier label.
+ *
+ * @return The identifier of the receiver.
+ */
+ public String identifier() {
+ return identifier;
+ }
+
+ /**
+ * Gets the retry policy when receiving events. If not specified, the retry policy configured on the associated
+ * {@link EventHubClient} is used.
+ *
+ * @return The retry policy when receiving events.
+ */
+ public Retry retry() {
+ return retryPolicy;
+ }
+
+ /**
+ * Gets the owner level for this consumer. If {@link Optional#isPresent()} is {@code false}, then this is not an
+ * exclusive consumer. Otherwise, it is an exclusive consumer, and there can only be one active consumer for each
+ * partition and consumer group combination.
+ *
+ * @return An optional owner level for this consumer.
+ */
+ public Long ownerLevel() {
+ return ownerLevel;
+ }
+
+ /**
+ * Gets the scheduler for reading events from Event Hubs. If not specified, the scheduler configured with the
+ * associated {@link EventHubClient} is used.
+ *
+ * @return The scheduler for reading events.
+ */
+ public Scheduler scheduler() {
+ return scheduler;
+ }
+
+ /**
+ * Gets the count used by the consumer to control the number of events this receiver will actively receive and queue
+ * locally without regard to whether a receive operation is currently active.
+ *
+ * @return The prefetch count receiver will receive and queue locally regardless of whether or not a receive
+ * operation is active.
+ */
+ public int prefetchCount() {
+ return prefetchCount;
+ }
+
+ /**
+ * Creates a shallow clone of this instance.
+ *
+ * The object is cloned, but this instance's fields are not cloned. {@link Duration} and {@link String} are
+ * immutable objects and are not an issue. The implementation of {@link Retry} could be mutable. In addition, the
+ * {@link #scheduler()} set is not cloned.
+ *
+ * @return A shallow clone of this object.
+ */
+ public EventHubConsumerOptions clone() {
+ EventHubConsumerOptions clone;
+ try {
+ clone = (EventHubConsumerOptions) super.clone();
+ } catch (CloneNotSupportedException e) {
+ clone = new EventHubConsumerOptions();
+ }
+
+ clone.scheduler(this.scheduler());
+ clone.identifier(this.identifier());
+ clone.prefetchCount(this.prefetchCount());
+ clone.retry(this.retry());
+ clone.ownerLevel(this.ownerLevel());
+
+ return clone;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducer.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducer.java
new file mode 100644
index 000000000000..3f9d221077cd
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducer.java
@@ -0,0 +1,314 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorCondition;
+import com.azure.core.implementation.util.ImplUtils;
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.messaging.eventhubs.implementation.AmqpSendLink;
+import com.azure.messaging.eventhubs.implementation.ErrorContextProvider;
+import com.azure.messaging.eventhubs.implementation.EventDataUtil;
+import org.apache.qpid.proton.message.Message;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+
+/**
+ * A producer responsible for transmitting {@link EventData} to a specific Event Hub, grouped together in batches.
+ * Depending on the options specified at creation, the producer may be created to allow event data to be automatically
+ * routed to an available partition or specific to a partition.
+ *
+ *
+ * Allowing automatic routing of partitions is recommended when:
+ *
+ *
The sending of events needs to be highly available.
+ *
The event data should be evenly distributed among all available partitions.
+ *
+ *
+ *
+ *
+ * If no partition is specified, the following rules are used for automatically selecting one:
+ *
+ *
Distribute the events equally amongst all available partitions using a round-robin approach.
+ *
If a partition becomes unavailable, the Event Hubs service will automatically detect it and forward the
+ * message to another available partition.
+ *
+ *
+ *
+ * @see EventHubClient#createProducer()
+ */
+public class EventHubProducer implements Closeable {
+ /**
+ * The default maximum allowable size, in bytes, for a batch to be sent.
+ */
+ public static final int MAX_MESSAGE_LENGTH_BYTES = 256 * 1024;
+
+ private static final int MAX_PARTITION_KEY_LENGTH = 128;
+ private static final SendOptions DEFAULT_SEND_OPTIONS = new SendOptions();
+
+ private final ClientLogger logger = new ClientLogger(EventHubProducer.class);
+ private final AtomicBoolean isDisposed = new AtomicBoolean();
+ private final EventHubProducerOptions senderOptions;
+ private final Mono sendLinkMono;
+ private final boolean isPartitionSender;
+
+ /**
+ * Creates a new instance of this {@link EventHubProducer} that sends messages to
+ * {@link EventHubProducerOptions#partitionId() options.partitionId()} if it is not {@code null} or an empty string,
+ * otherwise, allows the service to load balance the messages amongst available partitions.
+ */
+ EventHubProducer(Mono amqpSendLinkMono, EventHubProducerOptions options) {
+ // Caching the created link so we don't invoke another link creation.
+ this.sendLinkMono = amqpSendLinkMono.cache();
+ this.senderOptions = options;
+ this.isPartitionSender = !ImplUtils.isNullOrEmpty(options.partitionId());
+ }
+
+ /**
+ * Sends a single event to the associated Event Hub. If the size of the single event exceeds the maximum size
+ * allowed, an exception will be triggered and the send will fail.
+ *
+ * For more information regarding the maximum event size allowed, see
+ * Azure Event Hubs Quotas and Limits.
+ *
+ * @param event Event to send to the service.
+ * @return A {@link Mono} that completes when the event is pushed to the service.
+ */
+ public Mono send(EventData event) {
+ Objects.requireNonNull(event);
+
+ return send(Flux.just(event));
+ }
+
+ /**
+ * Sends a single event to the associated Event Hub with the send options. If the size of the single event exceeds
+ * the maximum size allowed, an exception will be triggered and the send will fail.
+ *
+ * For more information regarding the maximum event size allowed, see
+ * Azure Event Hubs Quotas and Limits.
+ *
+ * @param event Event to send to the service.
+ * @param options The set of options to consider when sending this event.
+ * @return A {@link Mono} that completes when the event is pushed to the service.
+ */
+ public Mono send(EventData event, SendOptions options) {
+ Objects.requireNonNull(event);
+ Objects.requireNonNull(options);
+
+ return send(Flux.just(event), options);
+ }
+
+ /**
+ * Sends a set of events to the associated Event Hub using a batched approach. If the size of events exceed the
+ * maximum size of a single batch, an exception will be triggered and the send will fail. By default, the message
+ * size is the max amount allowed on the link.
+ *
+ * @param events Events to send to the service.
+ * @return A {@link Mono} that completes when all events are pushed to the service.
+ */
+ public Mono send(Iterable events) {
+ Objects.requireNonNull(events);
+
+ return send(Flux.fromIterable(events));
+ }
+
+ /**
+ * Sends a set of events to the associated Event Hub using a batched approach. If the size of events exceed the
+ * maximum size of a single batch, an exception will be triggered and the send will fail. By default, the message
+ * size is the max amount allowed on the link.
+ *
+ * @param events Events to send to the service.
+ * @param options The set of options to consider when sending this batch.
+ * @return A {@link Mono} that completes when all events are pushed to the service.
+ */
+ public Mono send(Iterable events, SendOptions options) {
+ Objects.requireNonNull(events);
+
+ return send(Flux.fromIterable(events), options);
+ }
+
+ /**
+ * Sends a set of events to the associated Event Hub using a batched approach. If the size of events exceed the
+ * maximum size of a single batch, an exception will be triggered and the send will fail. By default, the message
+ * size is the max amount allowed on the link.
+ *
+ * @param events Events to send to the service.
+ * @return A {@link Mono} that completes when all events are pushed to the service.
+ */
+ public Mono send(Publisher events) {
+ Objects.requireNonNull(events);
+
+ return sendInternal(Flux.from(events), DEFAULT_SEND_OPTIONS);
+ }
+
+ /**
+ * Sends a set of events to the associated Event Hub using a batched approach. If the size of events exceed the
+ * maximum size of a single batch, an exception will be triggered and the send will fail. By default, the message
+ * size is the max amount allowed on the link.
+ *
+ * @param events Events to send to the service.
+ * @param options The set of options to consider when sending this batch.
+ * @return A {@link Mono} that completes when all events are pushed to the service.
+ */
+ public Mono send(Publisher events, SendOptions options) {
+ Objects.requireNonNull(events);
+ Objects.requireNonNull(options);
+
+ return sendInternal(Flux.from(events), options);
+ }
+
+ private Mono sendInternal(Flux events, SendOptions options) {
+ final String partitionKey = options.partitionKey();
+
+ if (!ImplUtils.isNullOrEmpty(partitionKey)) {
+ if (isPartitionSender) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "SendOptions.partitionKey() cannot be set when an EventSender is "
+ + "created with EventSenderOptions.partitionId() set. This EventSender can only send events to partition '%s'.",
+ senderOptions.partitionId()));
+ } else if (partitionKey.length() > MAX_PARTITION_KEY_LENGTH) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "PartitionKey '%s' exceeds the maximum allowed length: '%s'.", partitionKey, MAX_PARTITION_KEY_LENGTH));
+ }
+ }
+
+ //TODO (conniey): When we implement partial success, update the maximum number of batches or remove it completely.
+ return sendLinkMono.flatMap(link -> {
+ return events.collect(new EventDataCollector(options, 1, link::getErrorContext))
+ .flatMap(list -> send(Flux.fromIterable(list)));
+ });
+ }
+
+ private Mono send(Flux eventBatches) {
+ return eventBatches
+ .flatMap(this::send)
+ .then()
+ .doOnError(error -> {
+ logger.asError().log("Error sending batch.", error);
+ });
+ }
+
+ private Mono send(EventDataBatch batch) {
+ if (batch.getEvents().isEmpty()) {
+ logger.asInfo().log("Cannot send an EventBatch that is empty.");
+ return Mono.empty();
+ }
+
+ logger.asInfo().log("Sending with partitionKey[{}], batch size[{}]", batch.getPartitionKey(), batch.getSize());
+
+ final List messages = EventDataUtil.toAmqpMessage(batch.getPartitionKey(), batch.getEvents());
+
+ return sendLinkMono.flatMap(link -> messages.size() == 1
+ ? link.send(messages.get(0))
+ : link.send(messages));
+ }
+
+ /**
+ * Disposes of the {@link EventHubProducer} by closing the underlying connection to the service.
+ *
+ * @throws IOException if the underlying transport could not be closed and its resources could not be disposed.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!isDisposed.getAndSet(true)) {
+ final AmqpSendLink block = sendLinkMono.block(senderOptions.timeout());
+ if (block != null) {
+ block.close();
+ }
+ }
+ }
+
+ /**
+ * Collects EventData into EventDataBatch to send to Event Hubs. If {@code maxNumberOfBatches} is {@code null} then
+ * it'll collect as many batches as possible. Otherwise, if there are more events than can fit into
+ * {@code maxNumberOfBatches}, then the collector throws a {@link AmqpException} with
+ * {@link ErrorCondition#LINK_PAYLOAD_SIZE_EXCEEDED}.
+ */
+ private static class EventDataCollector implements Collector, List> {
+ private final String partitionKey;
+ private final int maxMessageSize;
+ private final Integer maxNumberOfBatches;
+ private final ErrorContextProvider contextProvider;
+
+ private volatile EventDataBatch currentBatch;
+
+ EventDataCollector(SendOptions options, Integer maxNumberOfBatches, ErrorContextProvider contextProvider) {
+ this.maxNumberOfBatches = maxNumberOfBatches;
+ this.maxMessageSize = options.maximumSizeInBytes();
+ this.partitionKey = options.partitionKey();
+ this.contextProvider = contextProvider;
+
+ currentBatch = new EventDataBatch(options.maximumSizeInBytes(), options.partitionKey(), contextProvider);
+ }
+
+ @Override
+ public Supplier> supplier() {
+ return ArrayList::new;
+ }
+
+ @Override
+ public BiConsumer, EventData> accumulator() {
+ return (list, event) -> {
+ EventDataBatch batch = currentBatch;
+ if (batch.tryAdd(event)) {
+ return;
+ }
+
+ if (maxNumberOfBatches != null && list.size() == maxNumberOfBatches) {
+ final String message = String.format(Locale.US,
+ "EventData does not fit into maximum number of batches. '%s'", maxNumberOfBatches);
+
+ throw new AmqpException(false, ErrorCondition.LINK_PAYLOAD_SIZE_EXCEEDED, message, contextProvider.getErrorContext());
+ }
+
+ currentBatch = new EventDataBatch(maxMessageSize, partitionKey, contextProvider);
+ currentBatch.tryAdd(event);
+ list.add(batch);
+ };
+ }
+
+ @Override
+ public BinaryOperator> combiner() {
+ return (existing, another) -> {
+ existing.addAll(another);
+ return existing;
+ };
+ }
+
+ @Override
+ public Function, List> finisher() {
+ return list -> {
+ EventDataBatch batch = currentBatch;
+ currentBatch = null;
+
+ if (batch != null) {
+ list.add(batch);
+ }
+
+ return list;
+ };
+ }
+
+ @Override
+ public Set characteristics() {
+ return Collections.emptySet();
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerOptions.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerOptions.java
new file mode 100644
index 000000000000..4993e27b4ba6
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerOptions.java
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.amqp.Retry;
+
+import java.time.Duration;
+
+/**
+ * The set of options that can be specified when creating an {@link EventHubProducer} to configure its behavior.
+ */
+public class EventHubProducerOptions implements Cloneable {
+ private String partitionId;
+ private Retry retry;
+ private Duration timeout;
+
+ /**
+ * Sets the identifier of the Event Hub partition that the {@link EventHubProducer} will be bound to, limiting it to
+ * sending events to only that partition.
+ *
+ * If the identifier is not specified, the Event Hubs service will be responsible for routing events that are sent
+ * to an available partition.
+ *
+ * @param partitionId The identifier of the Event Hub partition that the {@link EventHubProducer} will be bound to.
+ * If the producer wishes the events to be automatically to partitions, {@code null}; otherwise, the
+ * identifier of the desired partition.
+ * @return The updated {@link EventHubProducerOptions} object.
+ */
+ public EventHubProducerOptions partitionId(String partitionId) {
+ this.partitionId = partitionId;
+ return this;
+ }
+
+ /**
+ * Sets the retry policy used to govern retry attempts when an issue is encountered while sending.
+ *
+ * @param retry The retry policy used to govern retry attempts when an issue is encountered while sending.
+ * @return The updated SenderOptions object.
+ */
+ public EventHubProducerOptions retry(Retry retry) {
+ this.retry = retry;
+ return this;
+ }
+
+ /**
+ * Sets the default timeout to apply when sending events. If the timeout is reached, before the Event Hub
+ * acknowledges receipt of the event data being sent, the attempt will be considered failed and will be retried.
+ *
+ * @param timeout The timeout to apply when sending events.
+ * @return The updated {@link EventHubProducerOptions} object.
+ */
+ public EventHubProducerOptions timeout(Duration timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the retry policy used to govern retry attempts when an issue is encountered while sending.
+ *
+ * @return the retry policy used to govern retry attempts when an issue is encountered while sending. If
+ * {@code null}, then the retry policy configured on the associated {@link EventHubClient} is used.
+ */
+ public Retry retry() {
+ return retry;
+ }
+
+ /**
+ * Gets the identifier of the Event Hub partition that the {@link EventHubProducer} will be bound to, limiting it to
+ * sending events to only that partition.
+ *
+ * If the identifier is not specified, the Event Hubs service will be responsible for routing events that are sent
+ * to an available partition.
+ *
+ * @return the identifier of the Event Hub partition that the {@link EventHubProducer} will be bound to.
+ */
+ public String partitionId() {
+ return partitionId;
+ }
+
+ /**
+ * Gets the default timeout when sending events.
+ *
+ * @return The default timeout when sending events.
+ */
+ public Duration timeout() {
+ return timeout;
+ }
+
+ /**
+ * Creates a shallow clone of this instance.
+ *
+ * The object is cloned, but the objects {@link #retry()}, {@link #timeout()} and {@link #partitionId()} are not
+ * cloned. {@link Duration} and {@link String} are immutable objects and are not an issue. However, the
+ * implementation of {@link Retry} could be mutable.
+ *
+ * @return A shallow clone of this object.
+ */
+ public EventHubProducerOptions clone() {
+ EventHubProducerOptions clone;
+ try {
+ clone = (EventHubProducerOptions) super.clone();
+ } catch (CloneNotSupportedException e) {
+ clone = new EventHubProducerOptions();
+ }
+
+ clone.partitionId(this.partitionId);
+ clone.retry(this.retry);
+ clone.timeout(this.timeout);
+
+ return clone;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProperties.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProperties.java
new file mode 100644
index 000000000000..8e8060827733
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProperties.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import java.time.Instant;
+import java.util.Arrays;
+
+/**
+ * Holds information about Event Hubs which can come handy while performing data-plane operations like
+ * {@link EventHubClient#createConsumer(String, String, EventPosition)} and
+ * {@link EventHubClient#createConsumer(String, String, EventPosition, EventHubConsumerOptions)}.
+ */
+public final class EventHubProperties {
+ private final String path;
+ private final Instant createdAt;
+ private final String[] partitionIds;
+
+ EventHubProperties(
+ final String path,
+ final Instant createdAt,
+ final String[] partitionIds) {
+ this.path = path;
+ this.createdAt = createdAt;
+ this.partitionIds = partitionIds != null
+ ? Arrays.copyOf(partitionIds, partitionIds.length)
+ : new String[0];
+ }
+
+ /**
+ * Gets the Event Hub name
+ *
+ * @return Name of the Event Hub.
+ */
+ public String path() {
+ return path;
+ }
+
+ /**
+ * Gets the instant, in UTC, at which Event Hub was created at.
+ *
+ * @return The instant, in UTC, at which the Event Hub was created.
+ */
+ public Instant createdAt() {
+ return createdAt;
+ }
+
+ /**
+ * Gets the list of partition identifiers of the Event Hub.
+ *
+ * @return The list of partition identifiers of the Event Hub.
+ */
+ public String[] partitionIds() {
+ return partitionIds;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubSharedAccessKeyCredential.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubSharedAccessKeyCredential.java
new file mode 100644
index 000000000000..42222b70ba0f
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubSharedAccessKeyCredential.java
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.credentials.AccessToken;
+import com.azure.core.credentials.TokenCredential;
+import com.azure.core.implementation.util.ImplUtils;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Base64;
+import java.util.Locale;
+import java.util.Objects;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Authorizes with Azure Event Hubs service using a shared access key from either an Event Hubs namespace or a specific
+ * Event Hub.
+ */
+public class EventHubSharedAccessKeyCredential implements TokenCredential {
+ private static final String SHARED_ACCESS_SIGNATURE_FORMAT = "SharedAccessSignature sr=%s&sig=%s&se=%s&skn=%s";
+ private static final String HASH_ALGORITHM = "HMACSHA256";
+
+ private final String keyName;
+ private final Mac hmac;
+ private final Duration tokenValidity;
+
+ /**
+ * Creates an instance that authorizes using the {@code keyName} and {@code sharedAccessKey}. The authorization
+ * lasts for a period of {@code tokenValidity} before another token must be requested.
+ *
+ * @param keyName Name of the shared access key policy.
+ * @param sharedAccessKey Value of the shared access key.
+ * @param tokenValidity The duration for which the shared access signature is valid.
+ * @throws IllegalArgumentException if {@code keyName}, {@code sharedAccessKey} is null or empty. Or the duration of
+ * {@code tokenValidity} is zero or a negative value.
+ * @throws NoSuchAlgorithmException If the hashing algorithm cannot be instantiated, which is used to generate the
+ * shared access signatures.
+ * @throws InvalidKeyException If the {@code sharedAccessKey} is an invalid value for the hashing algorithm.
+ * @throws NullPointerException if {@code tokenValidity} is null.
+ */
+ public EventHubSharedAccessKeyCredential(String keyName, String sharedAccessKey, Duration tokenValidity)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+
+ if (ImplUtils.isNullOrEmpty(keyName)) {
+ throw new IllegalArgumentException("keyName cannot be null or empty");
+ }
+ if (ImplUtils.isNullOrEmpty(sharedAccessKey)) {
+ throw new IllegalArgumentException("sharedAccessKey cannot be null or empty.");
+ }
+
+ Objects.requireNonNull(tokenValidity);
+ if (tokenValidity.isZero() || tokenValidity.isNegative()) {
+ throw new IllegalArgumentException("tokenTimeToLive has to positive and in the order-of seconds");
+ }
+
+ this.keyName = keyName;
+ this.tokenValidity = tokenValidity;
+
+ hmac = Mac.getInstance(HASH_ALGORITHM);
+
+ final byte[] sasKeyBytes = sharedAccessKey.getBytes(UTF_8);
+ final SecretKeySpec finalKey = new SecretKeySpec(sasKeyBytes, HASH_ALGORITHM);
+ hmac.init(finalKey);
+ }
+
+ /**
+ * Retrieves the token, given the audience/resources requested, for use in authorization against an Event Hubs
+ * namespace or a specific Event Hub instance.
+ *
+ * @param scopes The name of the resource or token audience to obtain a token for.
+ * @return A Mono that completes and returns the shared access signature.
+ * @throws IllegalArgumentException if {@code scopes} does not contain a single value, which is the token audience.
+ */
+ @Override
+ public Mono getToken(String... scopes) {
+ if (scopes.length != 1) {
+ throw new IllegalArgumentException("'scopes' should only contain a single argument that is the token audience or resource name.");
+ }
+
+ return Mono.fromCallable(() -> generateSharedAccessSignature(scopes[0]));
+ }
+
+ private AccessToken generateSharedAccessSignature(final String resource) throws UnsupportedEncodingException {
+ if (ImplUtils.isNullOrEmpty(resource)) {
+ throw new IllegalArgumentException("resource cannot be empty");
+ }
+
+ final String utf8Encoding = UTF_8.name();
+ final OffsetDateTime expiresOn = OffsetDateTime.now(ZoneOffset.UTC).plus(tokenValidity);
+ final String expiresOnEpochSeconds = Long.toString(expiresOn.toEpochSecond());
+ final String audienceUri = URLEncoder.encode(resource, utf8Encoding);
+ final String secretToSign = audienceUri + "\n" + expiresOnEpochSeconds;
+
+ final byte[] signatureBytes = hmac.doFinal(secretToSign.getBytes(utf8Encoding));
+ final String signature = Base64.getEncoder().encodeToString(signatureBytes);
+
+ final String token = String.format(Locale.US, SHARED_ACCESS_SIGNATURE_FORMAT,
+ audienceUri,
+ URLEncoder.encode(signature, utf8Encoding),
+ URLEncoder.encode(expiresOnEpochSeconds, utf8Encoding),
+ URLEncoder.encode(keyName, utf8Encoding));
+
+ return new AccessToken(token, expiresOn);
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventPosition.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventPosition.java
new file mode 100644
index 000000000000..048f1bef6e25
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventPosition.java
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.messaging.eventhubs.implementation.AmqpConstants;
+
+import java.time.Instant;
+import java.util.Locale;
+import java.util.Objects;
+
+import static com.azure.core.amqp.MessageConstant.ENQUEUED_TIME_UTC_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.OFFSET_ANNOTATION_NAME;
+import static com.azure.core.amqp.MessageConstant.SEQUENCE_NUMBER_ANNOTATION_NAME;
+
+/**
+ * Defines a position of an {@link EventData} in the event hub partition.
+ * The position can be an Offset, Sequence Number, or EnqueuedTime.
+ */
+public final class EventPosition {
+ /**
+ * This is a constant defined to represent the start of a partition stream in EventHub.
+ */
+ private static final String START_OF_STREAM = "-1";
+
+ /**
+ * This is a constant defined to represent the current end of a partition stream in EventHub.
+ * This can be used as an offset argument in receiver creation to start receiving from the latest
+ * event, instead of a specific offset or point in time.
+ */
+ private static final String END_OF_STREAM = "@latest";
+
+ private static final EventPosition EARLIEST = fromOffset(START_OF_STREAM, false);
+ private static final EventPosition LATEST = fromOffset(END_OF_STREAM, false);
+
+ private final ClientLogger logger = new ClientLogger(EventPosition.class);
+ private final boolean isInclusive;
+ private String offset;
+ private Long sequenceNumber;
+ private Instant enqueuedDateTime;
+
+ private EventPosition(boolean isInclusive) {
+ this.isInclusive = isInclusive;
+ }
+
+ /**
+ * Returns the position for the start of a stream. Provide this position in receiver creation
+ * to start receiving from the first available (earliest) event in the partition.
+ *
+ * @return An {@link EventPosition} set to the start of an Event Hubs stream.
+ */
+ public static EventPosition earliest() {
+ return EARLIEST;
+ }
+
+ /**
+ * Corresponds to the end of the partition, where no more events are currently enqueued. Use this position to begin
+ * receiving from the next event to be enqueued in the partition after an {@link EventHubConsumer} is created with this
+ * position.
+ *
+ * @return An {@link EventPosition} set to the end of an Event Hubs stream and listens for new events.
+ */
+ public static EventPosition latest() {
+ return LATEST;
+ }
+
+ /**
+ * Creates a position at the given {@link Instant}. Corresponds to a specific instance within a partition to begin
+ * looking for an event. The event enqueued after the requested {@code enqueuedDateTime} becomes the current
+ * position.
+ *
+ * @param enqueuedDateTime The instant, in UTC, from which the next available event should be chosen.
+ * @return An {@link EventPosition} object.
+ */
+ public static EventPosition fromEnqueuedTime(Instant enqueuedDateTime) {
+ EventPosition position = new EventPosition(false);
+ position.enqueuedDateTime = enqueuedDateTime;
+ return position;
+ }
+
+ /**
+ * Corresponds to the event in the partition at the provided offset, inclusive of that event.
+ *
+ * @param offset The offset of the event within that partition.
+ * @return An {@link EventPosition} object.
+ */
+ public static EventPosition fromOffset(String offset) {
+ return fromOffset(offset, true);
+ }
+
+ /**
+ * Creates a position to an event in the partition at the provided offset. If {@code isInclusive} is true, the
+ * event with the same offset is returned. Otherwise, the next event is received.
+ *
+ * @param offset The offset of an event with respect to its relative position in the
+ * @param isInclusive If true, the event with the {@code offset} is included; otherwise, the next event will be
+ * received.
+ * @return An {@link EventPosition} object.
+ */
+ public static EventPosition fromOffset(String offset, boolean isInclusive) {
+ Objects.requireNonNull(offset);
+
+ EventPosition position = new EventPosition(isInclusive);
+ position.offset = offset;
+ return position;
+ }
+
+ /**
+ * Creates a position at the given sequence number. The specified event will not be included. Instead, the next
+ * event is returned.
+ *
+ * @param sequenceNumber is the sequence number of the event.
+ * @return An {@link EventPosition} object.
+ */
+ public static EventPosition fromSequenceNumber(long sequenceNumber) {
+ return fromSequenceNumber(sequenceNumber, false);
+ }
+
+ /**
+ * Creates a position at the given sequence number. If {@code isInclusive} is true, the event with the same sequence
+ * number is returned. Otherwise, the next event in the sequence is received.
+ *
+ * @param sequenceNumber is the sequence number of the event.
+ * @param isInclusive If true, the event with the {@code sequenceNumber} is included; otherwise, the next event will
+ * be received.
+ * @return An {@link EventPosition} object.
+ */
+ public static EventPosition fromSequenceNumber(long sequenceNumber, boolean isInclusive) {
+ EventPosition position = new EventPosition(isInclusive);
+ position.sequenceNumber = sequenceNumber;
+ return position;
+ }
+
+ String getExpression() {
+ final String isInclusiveFlag = isInclusive ? "=" : "";
+
+ // order of preference
+ if (this.offset != null) {
+ return String.format(AmqpConstants.AMQP_ANNOTATION_FORMAT, OFFSET_ANNOTATION_NAME.getValue(), isInclusiveFlag, this.offset);
+ }
+
+ if (this.sequenceNumber != null) {
+ return String.format(AmqpConstants.AMQP_ANNOTATION_FORMAT, SEQUENCE_NUMBER_ANNOTATION_NAME.getValue(), isInclusiveFlag, this.sequenceNumber);
+ }
+
+ if (this.enqueuedDateTime != null) {
+ String ms;
+ try {
+ ms = Long.toString(this.enqueuedDateTime.toEpochMilli());
+ } catch (ArithmeticException ex) {
+ ms = Long.toString(Long.MAX_VALUE);
+ logger.asWarning().log(
+ "Receiver not yet created, action[createReceiveLink], warning[starting receiver from epoch+Long.Max]");
+ }
+
+ return String.format(AmqpConstants.AMQP_ANNOTATION_FORMAT, ENQUEUED_TIME_UTC_ANNOTATION_NAME.getValue(), isInclusiveFlag, ms);
+ }
+
+ throw new IllegalArgumentException("No starting position was set.");
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "offset[%s], sequenceNumber[%s], enqueuedTime[%s], isInclusive[%s]",
+ offset, sequenceNumber,
+ enqueuedDateTime != null ? enqueuedDateTime.toEpochMilli() : "null",
+ isInclusive);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof EventPosition)) {
+ return false;
+ }
+
+ final EventPosition other = (EventPosition) obj;
+
+ return Objects.equals(isInclusive, other.isInclusive)
+ && Objects.equals(offset, other.offset)
+ && Objects.equals(sequenceNumber, other.sequenceNumber)
+ && Objects.equals(enqueuedDateTime, other.enqueuedDateTime);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isInclusive, offset, sequenceNumber, enqueuedDateTime);
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/PartitionProperties.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/PartitionProperties.java
new file mode 100644
index 000000000000..f478690b2486
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/PartitionProperties.java
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import java.time.Instant;
+
+/**
+ * Contains runtime information about an Event Hub partition.
+ */
+public final class PartitionProperties {
+ private final String eventHubPath;
+ private final String id;
+ private final long beginningSequenceNumber;
+ private final long lastEnqueuedSequenceNumber;
+ private final String lastEnqueuedOffset;
+ private final Instant lastEnqueuedTime;
+ private final boolean isEmpty;
+
+ PartitionProperties(
+ final String eventHubPath,
+ final String id,
+ final long beginningSequenceNumber,
+ final long lastEnqueuedSequenceNumber,
+ final String lastEnqueuedOffset,
+ final Instant lastEnqueuedTime,
+ final boolean isEmpty) {
+ this.eventHubPath = eventHubPath;
+ this.id = id;
+ this.beginningSequenceNumber = beginningSequenceNumber;
+ this.lastEnqueuedSequenceNumber = lastEnqueuedSequenceNumber;
+ this.lastEnqueuedOffset = lastEnqueuedOffset;
+ this.lastEnqueuedTime = lastEnqueuedTime;
+ this.isEmpty = isEmpty;
+ }
+
+ /**
+ * Gets the Event Hub path for this partition.
+ *
+ * @return The Event Hub path for this partition.
+ */
+ public String eventHubPath() {
+ return this.eventHubPath;
+ }
+
+ /**
+ * Gets the identifier of the partition within the Event Hub.
+ *
+ * @return The identifier of the partition within the Event Hub.
+ */
+ public String id() {
+ return this.id;
+ }
+
+ /**
+ * Gets the starting sequence number of the partition's message stream.
+ *
+ * @return The starting sequence number of the partition's message stream.
+ */
+ public long beginningSequenceNumber() {
+ return this.beginningSequenceNumber;
+ }
+
+ /**
+ * Gets the last sequence number of the partition's message stream.
+ *
+ * @return the last sequence number of the partition's message stream.
+ */
+ public long lastEnqueuedSequenceNumber() {
+ return this.lastEnqueuedSequenceNumber;
+ }
+
+ /**
+ * Gets the offset of the last enqueued message in the partition's stream.
+ *
+ * @return the offset of the last enqueued message in the partition's stream.
+ */
+ public String lastEnqueuedOffset() {
+ return this.lastEnqueuedOffset;
+ }
+
+ /**
+ * Gets the instant, in UTC, of the last enqueued message in the partition's stream.
+ *
+ * @return the instant, in UTC, of the last enqueued message in the partition's stream.
+ */
+ public Instant lastEnqueuedTime() {
+ return this.lastEnqueuedTime;
+ }
+
+ /**
+ * Indicates whether or not there are events in the partition.
+ *
+ * @return true if there are no events, and false otherwise.
+ */
+ public boolean isEmpty() {
+ return this.isEmpty;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyAuthenticationType.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyAuthenticationType.java
new file mode 100644
index 000000000000..9e3b77f46234
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyAuthenticationType.java
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+/**
+ * Supported methods of proxy authentication
+ */
+public enum ProxyAuthenticationType {
+ /**
+ * Proxy requires no authentication. Service calls will fail if proxy demands authentication.
+ */
+ NONE,
+ /**
+ * Authenticates against proxy with basic authentication scheme.
+ */
+ BASIC,
+ /**
+ * Authenticates against proxy with digest access authentication.
+ */
+ DIGEST,
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyConfiguration.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyConfiguration.java
new file mode 100644
index 000000000000..d3ba4ea04a2b
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/ProxyConfiguration.java
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+import com.azure.core.util.logging.ClientLogger;
+
+import java.net.PasswordAuthentication;
+import java.net.Proxy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Properties for configuring proxies with Event Hubs.
+ */
+public class ProxyConfiguration implements AutoCloseable {
+ /**
+ * The configuration key for containing the username who authenticates with the proxy.
+ */
+ public static final String PROXY_USERNAME = "PROXY_USERNAME";
+ /**
+ * The configuration key for containing the password for the username who authenticates with the proxy.
+ */
+ public static final String PROXY_PASSWORD = "PROXY_PASSWORD";
+
+ private final ClientLogger logger = new ClientLogger(ProxyConfiguration.class);
+ private final PasswordAuthentication credentials;
+ private final Proxy proxyAddress;
+ private final ProxyAuthenticationType authentication;
+
+ /**
+ * Gets the system defaults for proxy configuration and authentication.
+ */
+ public static final ProxyConfiguration SYSTEM_DEFAULTS = new ProxyConfiguration();
+
+ private ProxyConfiguration() {
+ this.credentials = null;
+ this.proxyAddress = null;
+ this.authentication = null;
+ }
+
+ /**
+ * Creates a proxy configuration that uses the {@code proxyAddress} and authenticates with provided
+ * {@code username}, {@code password} and {@code authentication}.
+ *
+ * @param authentication Authentication method to preemptively use with proxy.
+ * @param proxyAddress Proxy to use. If {@code null} is passed in, then the system configured {@link java.net.Proxy}
+ * is used.
+ * @param username Optional. Username used to authenticate with proxy. If not specified, the system-wide
+ * {@link java.net.Authenticator} is used to fetch credentials.
+ * @param password Optional. Password used to authenticate with proxy.
+ *
+ * @throws NullPointerException if {@code authentication} is {@code null}.
+ * @throws IllegalArgumentException if {@code authentication} is {@link ProxyAuthenticationType#BASIC} or
+ * {@link ProxyAuthenticationType#DIGEST} and {@code username} or {@code password} are {@code null}.
+ */
+ public ProxyConfiguration(ProxyAuthenticationType authentication, Proxy proxyAddress, String username, String password) {
+ Objects.requireNonNull(authentication);
+ this.authentication = authentication;
+ this.proxyAddress = proxyAddress;
+
+ if (username != null && password != null) {
+ this.credentials = new PasswordAuthentication(username, password.toCharArray());
+ } else {
+ logger.asInfo().log("Username or password is null. Using system-wide authentication.");
+ this.credentials = null;
+ }
+ }
+
+ /**
+ * Gets the proxy authentication type.
+ *
+ * @return the proxy authentication type to use. Returns {@code null} if no authentication type was set.
+ * This occurs when user uses {@link ProxyConfiguration#SYSTEM_DEFAULTS}.
+ */
+ public ProxyAuthenticationType authentication() {
+ return this.authentication;
+ }
+
+ /**
+ * Gets the proxy address.
+ *
+ * @return the proxy address. Return {@code null} if no proxy address was set
+ * This occurs when user uses {@link ProxyConfiguration#SYSTEM_DEFAULTS}.
+ */
+ public Proxy proxyAddress() {
+ return this.proxyAddress;
+ }
+
+ /**
+ * Gets the credentials user provided for authentication of proxy server.
+ *
+ * @return the username and password to use. Return {@code null} if no credential was set.
+ * This occurs when user uses {@link ProxyConfiguration#SYSTEM_DEFAULTS}.
+ */
+ public PasswordAuthentication credential() {
+ return this.credentials;
+ }
+
+ /**
+ * Gets whether the user has defined credentials.
+ *
+ * @return true if the user has defined the credentials to use, false otherwise.
+ */
+ public boolean hasUserDefinedCredentials() {
+ return credentials != null;
+ }
+
+ /**
+ * Gets whether the proxy address has been configured. Used to determine whether to use system-defined or
+ * user-defined proxy.
+ *
+ * @return true if the proxy url has been set, and false otherwise.
+ */
+ public boolean isProxyAddressConfigured() {
+ return proxyAddress != null && proxyAddress.address() != null;
+ }
+
+ /**
+ * Disposes of the configuration along with potential credentials.
+ */
+ @Override
+ public void close() {
+ if (credentials != null) {
+ Arrays.fill(credentials.getPassword(), '\0');
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/SendOptions.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/SendOptions.java
new file mode 100644
index 000000000000..7d6a4deabf08
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/SendOptions.java
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs;
+
+/**
+ * The set of options that can be specified when sending a set of events to configure how the event data is packaged
+ * into batches.
+ */
+public class SendOptions {
+ private int maximumSizeInBytes;
+ private String partitionKey;
+
+ /**
+ * Creates an instance with the maximum message size set to the maximum amount allowed by the protocol.
+ */
+ public SendOptions() {
+ this.maximumSizeInBytes = EventHubProducer.MAX_MESSAGE_LENGTH_BYTES;
+ }
+
+ /**
+ * Sets the maximum size to allow for a single batch of events, in bytes. If this size is exceeded, an exception
+ * will be thrown and the send operation will fail.
+ *
+ * @param maximumSizeInBytes The maximum size to allow for a single batch of events.
+ * @return The updated EventBatchingOptions object.
+ */
+ SendOptions maximumSizeInBytes(int maximumSizeInBytes) {
+ this.maximumSizeInBytes = maximumSizeInBytes;
+ return this;
+ }
+
+ /**
+ * Gets the maximum size to allow for a single batch of events, in bytes. If this size is exceeded, an exception
+ * will be thrown and the send operation will fail.
+ *
+ * @return The maximum size to allow for a single batch of events, in bytes.
+ */
+ int maximumSizeInBytes() {
+ return maximumSizeInBytes;
+ }
+
+ /**
+ * Sets a partition key on an event batch, which tells the Event Hubs service to send all events with that partition
+ * routing key to the same partition.
+ *
+ * @param partitionKey The label of an event batch.
+ * @return The updated EventBatchingOptions object.
+ */
+ public SendOptions partitionKey(String partitionKey) {
+ this.partitionKey = partitionKey;
+ return this;
+ }
+
+ /**
+ * Gets the partition routing key on an event batch. If specified, tells the Event Hubs service that these events
+ * belong to the same group and should belong to the same partition.
+ *
+ * @return The partition key on an event batch.
+ */
+ public String partitionKey() {
+ return partitionKey;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ActiveClientTokenManager.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ActiveClientTokenManager.java
new file mode 100644
index 000000000000..537bb1cfbf5a
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ActiveClientTokenManager.java
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.CBSNode;
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.AmqpResponseCode;
+import com.azure.core.exception.AzureException;
+import com.azure.core.util.logging.ClientLogger;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Manages the re-authorization of the client to the token audience against the CBS node.
+ */
+class ActiveClientTokenManager implements Closeable {
+ private final ClientLogger logger = new ClientLogger(ActiveClientTokenManager.class);
+ private final AtomicBoolean hasScheduled = new AtomicBoolean();
+ private final AtomicBoolean hasDisposed = new AtomicBoolean();
+ private final Mono cbsNode;
+ private final String tokenAudience;
+ private final Timer timer;
+ private final Flux authorizationResults;
+ private FluxSink sink;
+
+ // last refresh interval in milliseconds.
+ private AtomicLong lastRefreshInterval = new AtomicLong();
+
+ ActiveClientTokenManager(Mono cbsNode, String tokenAudience) {
+ this.timer = new Timer(tokenAudience + "-tokenManager");
+ this.cbsNode = cbsNode;
+ this.tokenAudience = tokenAudience;
+ this.authorizationResults = Flux.create(sink -> {
+ if (hasDisposed.get()) {
+ sink.complete();
+ } else {
+ this.sink = sink;
+ }
+ });
+
+ lastRefreshInterval.set(Duration.ofMinutes(1).getSeconds() * 1000);
+ }
+
+ /**
+ * Gets a flux of the periodic authorization results from the CBS node. Errors are returned on the Flux if
+ * authorization is unsuccessful.
+ *
+ * @return A Flux of authorization results from the CBS node.
+ */
+ Flux getAuthorizationResults() {
+ return authorizationResults;
+ }
+
+ /**
+ * Invokes an authorization call on the CBS node.
+ */
+ Mono authorize() {
+ if (hasDisposed.get()) {
+ return Mono.error(new AzureException("Cannot authorize with CBS node when this token manager has been disposed of."));
+ }
+
+ return cbsNode.flatMap(cbsNode -> cbsNode.authorize(tokenAudience))
+ .map(expiresOn -> {
+ if (!hasScheduled.getAndSet(true)) {
+ logger.asInfo().log("Scheduling refresh token.");
+ Duration between = Duration.between(OffsetDateTime.now(ZoneOffset.UTC), expiresOn);
+
+ // We want to refresh the token when 90% of the time before expiry has elapsed.
+ long refreshSeconds = (long) Math.floor(between.getSeconds() * 0.9);
+ long refreshIntervalMS = refreshSeconds * 1000;
+
+ lastRefreshInterval.set(refreshIntervalMS);
+
+ // This converts it to milliseconds
+ this.timer.schedule(new RefreshAuthorizationToken(), refreshIntervalMS);
+ }
+
+ return expiresOn;
+ }).then();
+ }
+
+ @Override
+ public void close() {
+ if (!hasDisposed.getAndSet(true)) {
+ this.timer.cancel();
+
+ if (this.sink != null) {
+ this.sink.complete();
+ }
+ }
+ }
+
+ private class RefreshAuthorizationToken extends TimerTask {
+ @Override
+ public void run() {
+ logger.asInfo().log("Refreshing authorization token.");
+ authorize().subscribe(
+ (Void response) -> {
+ logger.asInfo().log("Response acquired.");
+ }, error -> {
+ if ((error instanceof AmqpException) && ((AmqpException) error).isTransient()) {
+ logger.asError().log("Error is transient. Rescheduling authorization task.", error);
+ timer.schedule(new RefreshAuthorizationToken(), lastRefreshInterval.get());
+ } else {
+ logger.asError().log("Error occurred while refreshing token that is not retriable. Not scheduling"
+ + " refresh task. Use ActiveClientTokenManager.authorize() to schedule task again.", error);
+ hasScheduled.set(false);
+ }
+
+ sink.error(error);
+ }, () -> {
+ logger.asInfo().log("Success. Rescheduling refresh authorization task.");
+ sink.next(AmqpResponseCode.ACCEPTED);
+
+ if (hasScheduled.getAndSet(true)) {
+ timer.schedule(new RefreshAuthorizationToken(), lastRefreshInterval.get());
+ }
+ });
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpConstants.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpConstants.java
new file mode 100644
index 000000000000..55ca4745029f
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpConstants.java
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import org.apache.qpid.proton.amqp.Symbol;
+
+import static com.azure.core.amqp.MessageConstant.PARTITION_KEY_ANNOTATION_NAME;
+
+public final class AmqpConstants {
+ public static final String APACHE = "apache.org";
+ public static final String PROTON = "proton";
+ public static final String AMQP_ANNOTATION_FORMAT = "amqp.annotation.%s >%s '%s'";
+ public static final Symbol PARTITION_KEY = Symbol.getSymbol(PARTITION_KEY_ANNOTATION_NAME.getValue());
+
+ static final String VENDOR = "com.microsoft";
+
+ static final Symbol STRING_FILTER = Symbol.getSymbol(APACHE + ":selector-filter:string");
+
+ static final int AMQP_BATCH_MESSAGE_FORMAT = 0x80013700; // 2147563264L;
+
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpErrorCode.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpErrorCode.java
new file mode 100644
index 000000000000..7f53459decff
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpErrorCode.java
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.exception.ErrorCondition;
+import org.apache.qpid.proton.amqp.Symbol;
+
+/**
+ * AMQP error conditions mapped to proton-j symbols.
+ */
+public final class AmqpErrorCode {
+ public static final Symbol NOT_FOUND = Symbol.getSymbol(ErrorCondition.NOT_FOUND.getErrorCondition());
+ public static final Symbol UNAUTHORIZED_ACCESS = Symbol.getSymbol(ErrorCondition.UNAUTHORIZED_ACCESS.getErrorCondition());
+ public static final Symbol RESOURCE_LIMIT_EXCEEDED = Symbol.getSymbol(ErrorCondition.RESOURCE_LIMIT_EXCEEDED.getErrorCondition());
+ public static final Symbol NOT_ALLOWED = Symbol.getSymbol(ErrorCondition.NOT_ALLOWED.getErrorCondition());
+ public static final Symbol INTERNAL_ERROR = Symbol.getSymbol(ErrorCondition.INTERNAL_ERROR.getErrorCondition());
+ public static final Symbol ILLEGAL_STATE = Symbol.getSymbol(ErrorCondition.ILLEGAL_STATE.getErrorCondition());
+ public static final Symbol NOT_IMPLEMENTED = Symbol.getSymbol(ErrorCondition.NOT_IMPLEMENTED.getErrorCondition());
+
+ // link errors
+ public static final Symbol LINK_STOLEN = Symbol.getSymbol(ErrorCondition.LINK_STOLEN.getErrorCondition());
+ public static final Symbol LINK_PAYLOAD_SIZE_EXCEEDED = Symbol.getSymbol(ErrorCondition.LINK_PAYLOAD_SIZE_EXCEEDED.getErrorCondition());
+ public static final Symbol LINK_DETACH_FORCED = Symbol.getSymbol(ErrorCondition.LINK_DETACH_FORCED.getErrorCondition());
+
+ // connection errors
+ public static final Symbol CONNECTION_FORCED = Symbol.getSymbol(ErrorCondition.CONNECTION_FORCED.getErrorCondition());
+
+ // proton library introduced this AMQP symbol in their code-base to communicate IOExceptions
+ // while performing operations on SocketChannel (in IOHandler.java)
+ public static final Symbol PROTON_IO_ERROR = Symbol.getSymbol("proton:io");
+
+ public static final Symbol SERVER_BUSY_ERROR = Symbol.getSymbol(ErrorCondition.SERVER_BUSY_ERROR.getErrorCondition());
+ public static final Symbol ARGUMENT_ERROR = Symbol.getSymbol(ErrorCondition.ARGUMENT_ERROR.getErrorCondition());
+ public static final Symbol ARGUMENT_OUT_OF_RANGE_ERROR = Symbol.getSymbol(ErrorCondition.ARGUMENT_OUT_OF_RANGE_ERROR.getErrorCondition());
+ public static final Symbol ENTITY_DISABLED_ERROR = Symbol.getSymbol(ErrorCondition.ENTITY_DISABLED_ERROR.getErrorCondition());
+ public static final Symbol PARTITION_NOT_OWNED_ERROR = Symbol.getSymbol(ErrorCondition.PARTITION_NOT_OWNED_ERROR.getErrorCondition());
+ public static final Symbol STORE_LOCK_LOST_ERROR = Symbol.getSymbol(ErrorCondition.STORE_LOCK_LOST_ERROR.getErrorCondition());
+ public static final Symbol PUBLISHER_REVOKED_ERROR = Symbol.getSymbol(ErrorCondition.PUBLISHER_REVOKED_ERROR.getErrorCondition());
+ public static final Symbol TIMEOUT_ERROR = Symbol.getSymbol(ErrorCondition.TIMEOUT_ERROR.getErrorCondition());
+ public static final Symbol TRACKING_ID_PROPERTY = Symbol.getSymbol(ErrorCondition.TRACKING_ID_PROPERTY.getErrorCondition());
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLink.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLink.java
new file mode 100644
index 000000000000..b06f34b7cce4
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLink.java
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpLink;
+import org.apache.qpid.proton.message.Message;
+import reactor.core.publisher.Flux;
+
+import java.io.Closeable;
+import java.util.function.Supplier;
+
+/**
+ * A unidirectional link from the client to the message broker that listens for messages.
+ *
+ * @see AMQP
+ * Specification 1.0: Links
+ */
+public interface AmqpReceiveLink extends AmqpLink {
+ /**
+ * Initialises the link from the client to the message broker and begins to receive messages from the broker.
+ *
+ * @return A Flux of AMQP messages which completes when the client calls
+ * {@link Closeable#close() AmqpReceiveLink.close()} or an unrecoverable error occurs on the AMQP link.
+ */
+ Flux receive();
+
+ /**
+ * Adds the specified number of credits to the link.
+ *
+ * The number of link credits initialises to zero. It is the application's responsibility to call this method to
+ * allow the receiver to receive {@code credits} more deliveries.
+ *
+ * @param credits Number of credits to add to the receive link.
+ */
+ void addCredits(int credits);
+
+ /**
+ * Gets the current number of credits this link has.
+ *
+ * @return The number of credits (deliveries) this link has.
+ */
+ int getCredits();
+
+ /**
+ * Sets an event listener that is invoked when there are no credits on the link left. If the supplier returns an
+ * integer that is {@code null} or less than 1, then no credits are added to the link and no more messages are
+ * received on the link.
+ *
+ * @param creditSupplier Supplier that returns the number of credits to add to the link.
+ */
+ void setEmptyCreditListener(Supplier creditSupplier);
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpResponseMapper.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpResponseMapper.java
new file mode 100644
index 000000000000..564ec98a2773
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpResponseMapper.java
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.messaging.eventhubs.EventHubProperties;
+import com.azure.messaging.eventhubs.PartitionProperties;
+
+import java.util.Map;
+
+/**
+ * Mapper to help deserialize an AMQP message.
+ */
+public interface AmqpResponseMapper {
+ /**
+ * Deserialize the AMQP body to {@link EventHubProperties}.
+ *
+ * @param amqpBody AMQP response body to deserialize.
+ * @return The {@link EventHubProperties} represented by the AMQP body.
+ */
+ EventHubProperties toEventHubProperties(Map, ?> amqpBody);
+
+ /**
+ * Deserialize the AMQP body to {@link PartitionProperties}.
+ *
+ * @param amqpBody AMQP response body to deserialize.
+ * @return The {@link PartitionProperties} represented by the AMQP body.
+ */
+ PartitionProperties toPartitionProperties(Map, ?> amqpBody);
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpSendLink.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpSendLink.java
new file mode 100644
index 000000000000..4cbfed48a979
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpSendLink.java
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpLink;
+import com.azure.core.amqp.exception.AmqpException;
+import com.azure.core.amqp.exception.ErrorContext;
+import org.apache.qpid.proton.message.Message;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * An AMQP link that sends information to the remote endpoint.
+ */
+public interface AmqpSendLink extends AmqpLink {
+ /**
+ * Sends a single message to the remote endpoint.
+ *
+ * @param message Message to send.
+ * @return A Mono that completes when the message has been sent.
+ * @throws AmqpException if the serialized {@code message} exceed the links capacity for a single message.
+ */
+ Mono send(Message message);
+
+ /**
+ * Batches the messages given into a single proton-j message that is sent down the wire.
+ *
+ * @param messageBatch The batch of messages to send to the service.
+ * @return A Mono that completes when all the batched messages are successfully transmitted to Event Hub.
+ * @throws AmqpException if the serialized contents of {@code messageBatch} exceed the link's capacity for a single
+ * message.
+ */
+ Mono send(List messageBatch);
+
+ /**
+ * Gets the context for this AMQP send link.
+ */
+ ErrorContext getErrorContext();
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSAuthorizationType.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSAuthorizationType.java
new file mode 100644
index 000000000000..1824f4d17051
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSAuthorizationType.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.CBSNode;
+
+/**
+ * An enumeration of supported authorization methods with the {@link CBSNode}.
+ */
+public enum CBSAuthorizationType {
+ /**
+ * Authorize with CBS through a shared access signature.
+ */
+ SHARED_ACCESS_SIGNATURE("sastoken"),
+ /**
+ * Authorize with CBS using a JSON web token.
+ *
+ * This is used in the case where Azure Active Directory is used for authentication and the authenticated user
+ * wants to authorize with Azure Event Hubs.
+ */
+ JSON_WEB_TOKEN("jwt");
+
+ private final String scheme;
+
+ CBSAuthorizationType(String scheme) {
+ this.scheme = scheme;
+ }
+
+ /**
+ * Gets the token type scheme.
+ *
+ * @return The token type scheme.
+ */
+ public String getTokenType() {
+ return scheme;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSChannel.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSChannel.java
new file mode 100644
index 000000000000..d41405e1d585
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/CBSChannel.java
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpConnection;
+import com.azure.core.amqp.CBSNode;
+import com.azure.core.credentials.TokenCredential;
+import com.azure.core.util.logging.ClientLogger;
+import org.apache.qpid.proton.Proton;
+import org.apache.qpid.proton.amqp.messaging.AmqpValue;
+import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
+import org.apache.qpid.proton.message.Message;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+class CBSChannel extends EndpointStateNotifierBase implements CBSNode {
+ static final String SESSION_NAME = "cbs-session";
+ static final String CBS_ADDRESS = "$cbs";
+
+ private static final String LINK_NAME = "cbs";
+ private static final String PUT_TOKEN_OPERATION = "operation";
+ private static final String PUT_TOKEN_OPERATION_VALUE = "put-token";
+ private static final String PUT_TOKEN_TYPE = "type";
+ private static final String PUT_TOKEN_TYPE_VALUE_FORMAT = "servicebus.windows.net:%s";
+ private static final String PUT_TOKEN_AUDIENCE = "name";
+
+ private final AmqpConnection connection;
+ private final TokenCredential credential;
+ private final Mono cbsChannelMono;
+ private final ReactorProvider provider;
+ private final Duration operationTimeout;
+ private final CBSAuthorizationType authorizationType;
+
+ CBSChannel(AmqpConnection connection, TokenCredential tokenCredential, CBSAuthorizationType authorizationType,
+ ReactorProvider provider, ReactorHandlerProvider handlerProvider, Duration operationTimeout) {
+ super(new ClientLogger(CBSChannel.class));
+
+ Objects.requireNonNull(connection);
+ Objects.requireNonNull(tokenCredential);
+ Objects.requireNonNull(authorizationType);
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(operationTimeout);
+ Objects.requireNonNull(handlerProvider);
+
+ this.authorizationType = authorizationType;
+ this.operationTimeout = operationTimeout;
+ this.connection = connection;
+ this.credential = tokenCredential;
+ this.provider = provider;
+ this.cbsChannelMono = connection.createSession(SESSION_NAME)
+ .cast(ReactorSession.class)
+ .map(session -> new RequestResponseChannel(connection.getIdentifier(), connection.getHost(), LINK_NAME,
+ CBS_ADDRESS, session.session(), handlerProvider));
+ }
+
+ @Override
+ public Mono authorize(final String tokenAudience) {
+ final Message request = Proton.message();
+ final Map properties = new HashMap<>();
+ properties.put(PUT_TOKEN_OPERATION, PUT_TOKEN_OPERATION_VALUE);
+ properties.put(PUT_TOKEN_TYPE, String.format(Locale.ROOT, PUT_TOKEN_TYPE_VALUE_FORMAT, authorizationType.getTokenType()));
+ properties.put(PUT_TOKEN_AUDIENCE, tokenAudience);
+ final ApplicationProperties applicationProperties = new ApplicationProperties(properties);
+ request.setApplicationProperties(applicationProperties);
+
+ return credential.getToken(tokenAudience).flatMap(accessToken -> {
+ request.setBody(new AmqpValue(accessToken.token()));
+
+ return cbsChannelMono.flatMap(x -> x.sendWithAck(request, provider.getReactorDispatcher()))
+ .then(Mono.fromCallable(() -> accessToken.expiresOn()));
+ });
+ }
+
+ @Override
+ public void close() {
+ final RequestResponseChannel channel = cbsChannelMono.block(operationTimeout);
+ if (channel != null) {
+ channel.close();
+ }
+
+ if (!connection.removeSession(SESSION_NAME)) {
+ logger.asInfo().log("Unable to remove CBSChannel {} from connection", SESSION_NAME);
+ }
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ClientConstants.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ClientConstants.java
new file mode 100644
index 000000000000..24bea7a6cd9a
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ClientConstants.java
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import java.time.Duration;
+
+public final class ClientConstants {
+ public static final Duration OPERATION_TIMEOUT = Duration.ofSeconds(60);
+ public static final String NOT_APPLICABLE = "n/a";
+ public static final int HTTPS_PORT = 443;
+ public static final int MAX_EVENTHUB_AMQP_HEADER_SIZE_BYTES = 512;
+ public static final Duration TOKEN_VALIDITY = Duration.ofMinutes(20);
+ public static final int SERVER_BUSY_BASE_SLEEP_TIME_IN_SECS = 4;
+
+ public static final String PRODUCT_NAME = "azsdk-java-eventhubs";
+ public static final String CURRENT_JAVACLIENT_VERSION = "1.0.0-SNAPSHOT";
+ public static final String PLATFORM_INFO = getOSInformation();
+ public static final String FRAMEWORK_INFO = getFrameworkInfo();
+
+ /**
+ * Gets the USER AGENT string as defined in:
+ * $/core/azure-core/src/main/java/com/azure/core/http/policy/UserAgentPolicy.java
+ * TODO (conniey): Extract logic from UserAgentPolicy into something we can use here.
+ */
+ public static final String USER_AGENT = String.format("%s/%s %s;%s",
+ PRODUCT_NAME, CURRENT_JAVACLIENT_VERSION, System.getProperty("java.version"), PLATFORM_INFO);
+ public static final String HTTPS_URI_FORMAT = "https://%s:%s";
+ public static final String ENDPOINT_FORMAT = "sb://%s.%s";
+
+ private ClientConstants() {
+ }
+
+ private static String getOSInformation() {
+ return String.join(" ", System.getProperty("os.name"), System.getProperty("os.version"));
+ }
+
+ private static String getFrameworkInfo() {
+ final Package javaRuntimeClassPkg = Runtime.class.getPackage();
+
+ return "jre:" + javaRuntimeClassPkg.getImplementationVersion()
+ + ";vendor:" + javaRuntimeClassPkg.getImplementationVendor()
+ + ";jvm" + System.getProperty("java.vm.version");
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionOptions.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionOptions.java
new file mode 100644
index 000000000000..2f2d5745a820
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionOptions.java
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.Retry;
+import com.azure.core.amqp.TransportType;
+import com.azure.core.credentials.TokenCredential;
+import com.azure.messaging.eventhubs.ProxyConfiguration;
+import reactor.core.scheduler.Scheduler;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * A wrapper class that contains all parameters that are needed to establish a connection to an Event Hub.
+ */
+public class ConnectionOptions {
+ private final Duration timeout;
+ private final TokenCredential tokenCredential;
+ private final TransportType transport;
+ private final Retry retryPolicy;
+ private final ProxyConfiguration proxyConfiguration;
+ private final Scheduler scheduler;
+ private final String host;
+ private final String eventHubPath;
+ private final CBSAuthorizationType authorizationType;
+
+ public ConnectionOptions(String host, String eventHubPath, TokenCredential tokenCredential,
+ CBSAuthorizationType authorizationType, Duration timeout, TransportType transport,
+ Retry retryPolicy, ProxyConfiguration proxyConfiguration, Scheduler scheduler) {
+ Objects.requireNonNull(host);
+ Objects.requireNonNull(eventHubPath);
+ Objects.requireNonNull(timeout);
+ Objects.requireNonNull(tokenCredential);
+ Objects.requireNonNull(transport);
+ Objects.requireNonNull(retryPolicy);
+ Objects.requireNonNull(proxyConfiguration);
+ Objects.requireNonNull(scheduler);
+
+ this.host = host;
+ this.eventHubPath = eventHubPath;
+ this.timeout = timeout;
+ this.tokenCredential = tokenCredential;
+ this.authorizationType = authorizationType;
+ this.transport = transport;
+ this.retryPolicy = retryPolicy;
+ this.proxyConfiguration = proxyConfiguration;
+ this.scheduler = scheduler;
+ }
+
+ public String host() {
+ return host;
+ }
+
+ public String eventHubPath() {
+ return eventHubPath;
+ }
+
+ public Duration timeout() {
+ return timeout;
+ }
+
+ public TokenCredential tokenCredential() {
+ return tokenCredential;
+ }
+
+ public CBSAuthorizationType authorizationType() {
+ return authorizationType;
+ }
+
+ public TransportType transportType() {
+ return transport;
+ }
+
+ public Retry retryPolicy() {
+ return retryPolicy;
+ }
+
+ public ProxyConfiguration proxyConfiguration() {
+ return proxyConfiguration;
+ }
+
+ public Scheduler scheduler() {
+ return scheduler;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionStringProperties.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionStringProperties.java
new file mode 100644
index 000000000000..efd6eae2626a
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ConnectionStringProperties.java
@@ -0,0 +1,118 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.implementation.util.ImplUtils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+/**
+ * The set of properties that comprise a connection string from the Azure portal.
+ */
+public class ConnectionStringProperties {
+ private static final String TOKEN_VALUE_SEPARATOR = "=";
+ private static final String TOKEN_VALUE_PAIR_DELIMITER = ";";
+ private static final String ENDPOINT = "Endpoint";
+ private static final String SHARED_ACCESS_KEY_NAME = "SharedAccessKeyName";
+ private static final String SHARED_ACCESS_KEY = "SharedAccessKey";
+ private static final String ENTITY_PATH = "EntityPath";
+ private static final String ERROR_MESSAGE_FORMAT = "Could not parse 'connectionString'. Expected format: "
+ + "'Endpoint={endpoint};SharedAccessKeyName={sharedAccessKeyName};"
+ + "SharedAccessKey={sharedAccessKey};EntityPath={eventHubPath}'. Actual: %s";
+
+ private final URI endpoint;
+ private final String eventHubPath;
+ private final String sharedAccessKeyName;
+ private final String sharedAccessKey;
+
+ /**
+ * Creates a new instance by parsing the {@code connectionString} into its components.
+ *
+ * @param connectionString The connection string to the Event Hub instance.
+ * @throws IllegalArgumentException if {@code connectionString} is {@code null} or empty, the connection string has
+ * an invalid format.
+ */
+ public ConnectionStringProperties(String connectionString) {
+ if (ImplUtils.isNullOrEmpty(connectionString)) {
+ throw new IllegalArgumentException("'connectionString' cannot be null or empty");
+ }
+
+ final String[] tokenValuePairs = connectionString.split(TOKEN_VALUE_PAIR_DELIMITER);
+ URI endpoint = null;
+ String eventHubPath = null;
+ String sharedAccessKeyName = null;
+ String sharedAccessKeyValue = null;
+
+ for (String tokenValuePair : tokenValuePairs) {
+ final String[] pair = tokenValuePair.split(TOKEN_VALUE_SEPARATOR, 2);
+ if (pair.length != 2) {
+ throw new IllegalArgumentException(String.format(Locale.US, "Connection string has invalid key value pair: %s", tokenValuePair));
+ }
+
+ final String key = pair[0].trim();
+ final String value = pair[1].trim();
+
+ if (key.equalsIgnoreCase(ENDPOINT)) {
+ try {
+ endpoint = new URI(value);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(String.format(Locale.US, "Invalid endpoint: %s", tokenValuePair), e);
+ }
+ } else if (key.equalsIgnoreCase(SHARED_ACCESS_KEY_NAME)) {
+ sharedAccessKeyName = value;
+ } else if (key.equalsIgnoreCase(SHARED_ACCESS_KEY)) {
+ sharedAccessKeyValue = value;
+ } else if (key.equalsIgnoreCase(ENTITY_PATH)) {
+ eventHubPath = value;
+ }
+ }
+
+ if (endpoint == null || sharedAccessKeyName == null || sharedAccessKeyValue == null || eventHubPath == null) {
+ throw new IllegalArgumentException(String.format(Locale.US, ERROR_MESSAGE_FORMAT, connectionString));
+ }
+
+ this.endpoint = endpoint;
+ this.eventHubPath = eventHubPath;
+ this.sharedAccessKeyName = sharedAccessKeyName;
+ this.sharedAccessKey = sharedAccessKeyValue;
+ }
+
+ /**
+ * Gets the endpoint to be used for connecting to the Event Hubs namespace.
+ *
+ * @return The endpoint address, including protocol, from the connection string.
+ */
+ public URI endpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Gets the name of the specific Event Hub under the namespace.
+ *
+ * @return The name of the specific Event Hub under the namespace.
+ */
+ public String eventHubPath() {
+ return eventHubPath;
+ }
+
+ /**
+ * Gets the name of the shared access key, either for the Event Hubs namespace or the Event Hub instance.
+ *
+ * @return The name of the shared access key.
+ */
+ public String sharedAccessKeyName() {
+ return sharedAccessKeyName;
+ }
+
+ /**
+ * The value of the shared access key, either for the Event Hubs namespace or the Event Hub.
+ *
+ * @return The value of the shared access key.
+ */
+ public String sharedAccessKey() {
+ return sharedAccessKey;
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EndpointStateNotifierBase.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EndpointStateNotifierBase.java
new file mode 100644
index 000000000000..9d221fa09175
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EndpointStateNotifierBase.java
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpEndpointState;
+import com.azure.core.amqp.AmqpShutdownSignal;
+import com.azure.core.amqp.EndpointStateNotifier;
+import com.azure.core.util.logging.ClientLogger;
+import org.apache.qpid.proton.engine.EndpointState;
+import reactor.core.Disposable;
+import reactor.core.publisher.DirectProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.ReplayProcessor;
+
+import java.io.Closeable;
+import java.util.Objects;
+
+abstract class EndpointStateNotifierBase implements EndpointStateNotifier, Closeable {
+ private final ReplayProcessor connectionStateProcessor = ReplayProcessor.cacheLastOrDefault(AmqpEndpointState.UNINITIALIZED);
+ private final DirectProcessor errorContextProcessor = DirectProcessor.create();
+ private final DirectProcessor shutdownSignalProcessor = DirectProcessor.create();
+ private final Disposable subscription;
+
+ protected ClientLogger logger;
+ private volatile AmqpEndpointState state;
+
+ EndpointStateNotifierBase(ClientLogger logger) {
+ Objects.requireNonNull(logger);
+
+ this.logger = logger;
+ this.subscription = connectionStateProcessor.subscribe(s -> this.state = s);
+ }
+
+ @Override
+ public AmqpEndpointState getCurrentState() {
+ return state;
+ }
+
+ @Override
+ public Flux getErrors() {
+ return errorContextProcessor;
+ }
+
+ @Override
+ public Flux getConnectionStates() {
+ return connectionStateProcessor;
+ }
+
+ @Override
+ public Flux getShutdownSignals() {
+ return shutdownSignalProcessor;
+ }
+
+ void notifyError(Throwable error) {
+ Objects.requireNonNull(error);
+
+ logger.asInfo().log("Notify error: {}", error);
+ errorContextProcessor.onNext(error);
+ }
+
+ void notifyShutdown(AmqpShutdownSignal shutdownSignal) {
+ Objects.requireNonNull(shutdownSignal);
+
+ logger.asInfo().log("Notify shutdown signal: {}", shutdownSignal);
+ shutdownSignalProcessor.onNext(shutdownSignal);
+ }
+
+ void notifyEndpointState(EndpointState endpointState) {
+ Objects.requireNonNull(endpointState);
+
+ logger.asVerbose().log("Connection state: {}", endpointState);
+ final AmqpEndpointState state = getConnectionState(endpointState);
+ connectionStateProcessor.onNext(state);
+ }
+
+ private static AmqpEndpointState getConnectionState(EndpointState state) {
+ switch (state) {
+ case ACTIVE:
+ return AmqpEndpointState.ACTIVE;
+ case UNINITIALIZED:
+ return AmqpEndpointState.UNINITIALIZED;
+ case CLOSED:
+ return AmqpEndpointState.CLOSED;
+ default:
+ throw new UnsupportedOperationException("This endpoint state is not supported. State:" + state);
+ }
+ }
+
+ @Override
+ public void close() {
+ subscription.dispose();
+ connectionStateProcessor.onComplete();
+ errorContextProcessor.onComplete();
+ shutdownSignalProcessor.onComplete();
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ErrorContextProvider.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ErrorContextProvider.java
new file mode 100644
index 000000000000..ccaa4bb70a30
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ErrorContextProvider.java
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.exception.ErrorContext;
+import com.azure.core.amqp.exception.LinkErrorContext;
+import com.azure.core.amqp.exception.SessionErrorContext;
+
+/**
+ * Generates contexts for AMQP errors that occur on a connection, link, or session handler.
+ *
+ * @see SessionErrorContext
+ * @see LinkErrorContext
+ */
+@FunctionalInterface
+public interface ErrorContextProvider {
+ /**
+ * Gets the context this error occurred on.
+ *
+ * @return The context where this exception occurred.
+ */
+ ErrorContext getErrorContext();
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventDataUtil.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventDataUtil.java
new file mode 100644
index 000000000000..58cf8c9c76d9
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventDataUtil.java
@@ -0,0 +1,242 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.MessageConstant;
+import com.azure.core.implementation.util.ImplUtils;
+import com.azure.messaging.eventhubs.EventData;
+import org.apache.qpid.proton.Proton;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.AmqpValue;
+import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
+import org.apache.qpid.proton.amqp.messaging.Data;
+import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
+import org.apache.qpid.proton.message.Message;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class for converting {@link EventData} to {@link Message}.
+ */
+public class EventDataUtil {
+ /**
+ * Maps the set of events given to a collection of AMQP messages.
+ */
+ public static List toAmqpMessage(String partitionKey, List events) {
+ return events.stream().map(event -> toAmqpMessage(partitionKey, event)).collect(Collectors.toList());
+ }
+
+ /**
+ * Gets the serialized size of the AMQP message.
+ */
+ static int getDataSerializedSize(Message amqpMessage) {
+
+ if (amqpMessage == null) {
+ return 0;
+ }
+
+ int payloadSize = getPayloadSize(amqpMessage);
+
+ // EventData - accepts only PartitionKey - which is a String & stuffed into MessageAnnotation
+ final MessageAnnotations messageAnnotations = amqpMessage.getMessageAnnotations();
+ final ApplicationProperties applicationProperties = amqpMessage.getApplicationProperties();
+
+ int annotationsSize = 0;
+ int applicationPropertiesSize = 0;
+
+ if (messageAnnotations != null) {
+ final Map map = messageAnnotations.getValue();
+
+ for (Map.Entry entry : map.entrySet()) {
+ final int size = sizeof(entry.getKey()) + sizeof(entry.getValue());
+ annotationsSize += size;
+ }
+ }
+
+ if (applicationProperties != null) {
+ final Map map = applicationProperties.getValue();
+
+ for (Map.Entry entry : map.entrySet()) {
+ final int size = sizeof(entry.getKey()) + sizeof(entry.getValue());
+ applicationPropertiesSize += size;
+ }
+ }
+
+ return annotationsSize + applicationPropertiesSize + payloadSize;
+ }
+
+ /**
+ * Creates the AMQP message represented by this EventData.
+ *
+ * @return A new AMQP message for this EventData.
+ */
+ private static Message toAmqpMessage(String partitionKey, EventData eventData) {
+ final Message message = Proton.message();
+
+ if (eventData.properties() != null && !eventData.properties().isEmpty()) {
+ message.setApplicationProperties(new ApplicationProperties(eventData.properties()));
+ }
+
+ if (!ImplUtils.isNullOrEmpty(partitionKey)) {
+ final MessageAnnotations messageAnnotations = message.getMessageAnnotations() == null
+ ? new MessageAnnotations(new HashMap<>())
+ : message.getMessageAnnotations();
+ messageAnnotations.getValue().put(AmqpConstants.PARTITION_KEY, partitionKey);
+ message.setMessageAnnotations(messageAnnotations);
+ }
+
+ setSystemProperties(eventData, message);
+
+ if (eventData.body() != null) {
+ message.setBody(new Data(Binary.create(eventData.body())));
+ }
+
+ return message;
+ }
+
+ /*
+ * Sets AMQP protocol header values on the AMQP message.
+ */
+ private static void setSystemProperties(EventData eventData, Message message) {
+ if (eventData.systemProperties() == null || eventData.systemProperties().isEmpty()) {
+ return;
+ }
+
+ eventData.systemProperties().forEach((key, value) -> {
+ if (EventData.RESERVED_SYSTEM_PROPERTIES.contains(key)) {
+ return;
+ }
+
+ final MessageConstant constant = MessageConstant.fromString(key);
+
+ if (constant != null) {
+ switch (constant) {
+ case MESSAGE_ID:
+ message.setMessageId(value);
+ break;
+ case USER_ID:
+ message.setUserId((byte[]) value);
+ break;
+ case TO:
+ message.setAddress((String) value);
+ break;
+ case SUBJECT:
+ message.setSubject((String) value);
+ break;
+ case REPLY_TO:
+ message.setReplyTo((String) value);
+ break;
+ case CORRELATION_ID:
+ message.setCorrelationId(value);
+ break;
+ case CONTENT_TYPE:
+ message.setContentType((String) value);
+ break;
+ case CONTENT_ENCODING:
+ message.setContentEncoding((String) value);
+ break;
+ case ABSOLUTE_EXPRITY_TIME:
+ message.setExpiryTime((long) value);
+ break;
+ case CREATION_TIME:
+ message.setCreationTime((long) value);
+ break;
+ case GROUP_ID:
+ message.setGroupId((String) value);
+ break;
+ case GROUP_SEQUENCE:
+ message.setGroupSequence((long) value);
+ break;
+ case REPLY_TO_GROUP_ID:
+ message.setReplyToGroupId((String) value);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format(Locale.US, "Property is not a recognized reserved property name: %s", key));
+ }
+ } else {
+ final MessageAnnotations messageAnnotations = (message.getMessageAnnotations() == null)
+ ? new MessageAnnotations(new HashMap<>())
+ : message.getMessageAnnotations();
+ messageAnnotations.getValue().put(Symbol.getSymbol(key), value);
+ message.setMessageAnnotations(messageAnnotations);
+ }
+ });
+ }
+
+ private static int getPayloadSize(Message msg) {
+
+ if (msg == null || msg.getBody() == null) {
+ return 0;
+ }
+
+ if (msg.getBody() instanceof Data) {
+ final Data payloadSection = (Data) msg.getBody();
+ if (payloadSection == null) {
+ return 0;
+ }
+
+ final Binary payloadBytes = payloadSection.getValue();
+ if (payloadBytes == null) {
+ return 0;
+ }
+
+ return payloadBytes.getLength();
+ }
+
+ if (msg.getBody() instanceof AmqpValue) {
+ final AmqpValue amqpValue = (AmqpValue) msg.getBody();
+ if (amqpValue == null) {
+ return 0;
+ }
+
+ return amqpValue.getValue().toString().length() * 2;
+ }
+
+ return 0;
+ }
+
+ private static int sizeof(Object obj) {
+ if (obj instanceof String) {
+ return obj.toString().length() << 1;
+ }
+
+ if (obj instanceof Symbol) {
+ return ((Symbol) obj).length() << 1;
+ }
+
+ if (obj instanceof Integer) {
+ return Integer.BYTES;
+ }
+
+ if (obj instanceof Long) {
+ return Long.BYTES;
+ }
+
+ if (obj instanceof Short) {
+ return Short.BYTES;
+ }
+
+ if (obj instanceof Character) {
+ return Character.BYTES;
+ }
+
+ if (obj instanceof Float) {
+ return Float.BYTES;
+ }
+
+ if (obj instanceof Double) {
+ return Double.BYTES;
+ }
+
+ throw new IllegalArgumentException(String.format(Locale.US, "Encoding Type: %s is not supported", obj.getClass()));
+ }
+
+ private EventDataUtil() {
+ }
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnection.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnection.java
new file mode 100644
index 000000000000..21a0e017f9e7
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnection.java
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpConnection;
+import reactor.core.publisher.Mono;
+
+/**
+ * A connection to a specific Event Hub resource in Azure Event Hubs.
+ */
+public interface EventHubConnection extends AmqpConnection {
+ /**
+ * Gets the management node for fetching metadata about the Event Hub and performing management operations.
+ *
+ * @return A Mono that completes with a session to the Event Hub's management node.
+ */
+ Mono getManagementNode();
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubManagementNode.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubManagementNode.java
new file mode 100644
index 000000000000..871ec49a8a0f
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubManagementNode.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.EndpointStateNotifier;
+import com.azure.messaging.eventhubs.EventHubProperties;
+import com.azure.messaging.eventhubs.PartitionProperties;
+import reactor.core.publisher.Mono;
+
+import java.io.Closeable;
+
+/**
+ * The management node for fetching metadata about the Event Hub and its partitions.
+ */
+public interface EventHubManagementNode extends EndpointStateNotifier, Closeable {
+ /**
+ * Gets the metadata associated with the Event Hub.
+ *
+ * @return Metadata associated with the Event Hub.
+ */
+ Mono getEventHubProperties();
+
+ /**
+ * Gets the metadata associated with a particular partition in the Event Hub.
+ *
+ * @param partitionId The identifier of the partition.
+ * @return The metadata associated with the partition.
+ */
+ Mono getPartitionProperties(String partitionId);
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubSession.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubSession.java
new file mode 100644
index 000000000000..8b1760894cb0
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubSession.java
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpLink;
+import com.azure.core.amqp.AmqpSession;
+import com.azure.core.amqp.Retry;
+import com.azure.messaging.eventhubs.EventHubConsumer;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+/**
+ * Represents an AMQP session that supports vendor specific properties and capabilities. For example, creating a
+ * receiver that exclusively listens to a partition + consumer group combination, or getting snapshots of partition
+ * information.
+ *
+ * @see AmqpSession
+ * @see ReactorSession
+ */
+public interface EventHubSession extends AmqpSession {
+ /**
+ * Creates a new AMQP consumer.
+ *
+ * @param linkName Name of the sender link.
+ * @param entityPath The entity path this link connects to receive events.
+ * @param eventPositionExpression The position within the partition where the consumer should begin reading events.
+ * @param timeout Timeout required for creating and opening AMQP link.
+ * @param retry The retry policy to use when receiving messages.
+ * @param ownerLevel {@code null} if multiple {@link EventHubConsumer EventHubConsumers} can listen to the same
+ * partition and consumer group. Otherwise, the {@code receiverPriority} that is the highest will listen to
+ * that partition exclusively.
+ * @param consumerIdentifier Identifier for the consumer that is sent to the service.
+ * @return A newly created AMQP link.
+ */
+ Mono createConsumer(String linkName, String entityPath, String eventPositionExpression, Duration timeout,
+ Retry retry, Long ownerLevel, String consumerIdentifier);
+}
diff --git a/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ManagementChannel.java b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ManagementChannel.java
new file mode 100644
index 000000000000..61bdab1667c0
--- /dev/null
+++ b/eventhubs/client/azure-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/ManagementChannel.java
@@ -0,0 +1,162 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.messaging.eventhubs.implementation;
+
+import com.azure.core.amqp.AmqpConnection;
+import com.azure.core.credentials.TokenCredential;
+import com.azure.core.util.logging.ClientLogger;
+import com.azure.messaging.eventhubs.EventHubProperties;
+import com.azure.messaging.eventhubs.PartitionProperties;
+import org.apache.qpid.proton.Proton;
+import org.apache.qpid.proton.amqp.messaging.AmqpValue;
+import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
+import org.apache.qpid.proton.message.Message;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Channel responsible for Event Hubs related metadata and management plane operations. Management plane operations
+ * include another partition, increasing quotas, etc.
+ */
+public class ManagementChannel extends EndpointStateNotifierBase implements EventHubManagementNode {
+ // Well-known keys from the management service responses and requests.
+ public static final String MANAGEMENT_ENTITY_NAME_KEY = "name";
+ public static final String MANAGEMENT_PARTITION_NAME_KEY = "partition";
+ public static final String MANAGEMENT_RESULT_PARTITION_IDS = "partition_ids";
+ public static final String MANAGEMENT_RESULT_CREATED_AT = "created_at";
+ public static final String MANAGEMENT_RESULT_BEGIN_SEQUENCE_NUMBER = "begin_sequence_number";
+ public static final String MANAGEMENT_RESULT_LAST_ENQUEUED_SEQUENCE_NUMBER = "last_enqueued_sequence_number";
+ public static final String MANAGEMENT_RESULT_LAST_ENQUEUED_OFFSET = "last_enqueued_offset";
+ public static final String MANAGEMENT_RESULT_LAST_ENQUEUED_TIME_UTC = "last_enqueued_time_utc";
+ public static final String MANAGEMENT_RESULT_PARTITION_IS_EMPTY = "is_partition_empty";
+
+ private static final String SESSION_NAME = "mgmt-session";
+ private static final String LINK_NAME = "mgmt";
+ private static final String ADDRESS = "$management";
+
+ // Well-known keys for management plane service requests.
+ private static final String MANAGEMENT_ENTITY_TYPE_KEY = "type";
+ private static final String MANAGEMENT_OPERATION_KEY = "operation";
+ private static final String MANAGEMENT_SECURITY_TOKEN_KEY = "security_token";
+
+ // Well-known values for the service request.
+ private static final String READ_OPERATION_VALUE = "READ";
+ private static final String MANAGEMENT_EVENTHUB_ENTITY_TYPE = AmqpConstants.VENDOR + ":eventhub";
+ private static final String MANAGEMENT_PARTITION_ENTITY_TYPE = AmqpConstants.VENDOR + ":partition";
+
+ private final AmqpConnection connection;
+ private final TokenCredential tokenProvider;
+ private final Mono channelMono;
+ private final ReactorProvider provider;
+ private final String eventHubPath;
+ private final AmqpResponseMapper mapper;
+ private final TokenResourceProvider audienceProvider;
+
+ /**
+ * Creates an instance that is connected to the {@code eventHubPath}'s management node.
+ *
+ * @param eventHubPath The name of the Event Hub.
+ * @param tokenProvider A provider that generates authorization tokens.
+ * @param provider The dispatcher to execute work on Reactor.
+ */
+ ManagementChannel(AmqpConnection connection, String eventHubPath, TokenCredential tokenProvider,
+ TokenResourceProvider audienceProvider, ReactorProvider provider,
+ ReactorHandlerProvider handlerProvider, AmqpResponseMapper mapper) {
+ super(new ClientLogger(ManagementChannel.class));
+
+ Objects.requireNonNull(connection);
+ Objects.requireNonNull(eventHubPath);
+ Objects.requireNonNull(tokenProvider);
+ Objects.requireNonNull(audienceProvider);
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(handlerProvider);
+ Objects.requireNonNull(mapper);
+
+ this.audienceProvider = audienceProvider;
+ this.connection = connection;
+ this.tokenProvider = tokenProvider;
+ this.provider = provider;
+ this.eventHubPath = eventHubPath;
+ this.mapper = mapper;
+ this.channelMono = connection.createSession(SESSION_NAME)
+ .cast(ReactorSession.class)
+ .map(session -> new RequestResponseChannel(connection.getIdentifier(), connection.getHost(), LINK_NAME,
+ ADDRESS, session.session(), handlerProvider))
+ .cache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Mono getEventHubProperties() {
+ final Map properties = new HashMap<>();
+ properties.put(MANAGEMENT_ENTITY_TYPE_KEY, MANAGEMENT_EVENTHUB_ENTITY_TYPE);
+ properties.put(MANAGEMENT_ENTITY_NAME_KEY, eventHubPath);
+ properties.put(MANAGEMENT_OPERATION_KEY, READ_OPERATION_VALUE);
+
+ return getProperties(properties, mapper::toEventHubProperties);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Mono getPartitionProperties(String partitionId) {
+ final Map properties = new HashMap<>();
+ properties.put(MANAGEMENT_ENTITY_TYPE_KEY, MANAGEMENT_PARTITION_ENTITY_TYPE);
+ properties.put(MANAGEMENT_ENTITY_NAME_KEY, eventHubPath);
+ properties.put(MANAGEMENT_PARTITION_NAME_KEY, partitionId);
+ properties.put(MANAGEMENT_OPERATION_KEY, READ_OPERATION_VALUE);
+
+ return getProperties(properties, mapper::toPartitionProperties);
+ }
+
+ private Mono getProperties(Map properties, Function