From 378fb1d7a94ce7334bbaad1d3d0e3c555d34b6a2 Mon Sep 17 00:00:00 2001 From: Connie Date: Sun, 1 Dec 2019 12:06:46 -0800 Subject: [PATCH 01/31] Adding EventHubConnectionProcessor. --- .../eventhubs/EventHubConnection.java | 24 +- .../EventHubConnectionProcessor.java | 228 ++++++++++++++++++ 2 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java index 4b4c41c76797..3c4342d8d5c5 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java @@ -3,7 +3,6 @@ package com.azure.messaging.eventhubs; -import com.azure.core.amqp.AmqpConnection; import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpRetryPolicy; import com.azure.core.amqp.exception.AmqpException; @@ -13,6 +12,7 @@ import com.azure.core.amqp.implementation.RetryUtil; import com.azure.core.util.logging.ClientLogger; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; @@ -29,16 +29,14 @@ class EventHubConnection implements Closeable { private final ClientLogger logger = new ClientLogger(EventHubConnection.class); private final AtomicBoolean hasConnection = new AtomicBoolean(); private final ConnectionOptions connectionOptions; - private final Mono currentConnection; + private final EventHubConnectionProcessor eventHubConnectionProcessor; /** * Creates a new instance of {@link EventHubConnection}. */ EventHubConnection(Mono createConnectionMono, ConnectionOptions connectionOptions) { this.connectionOptions = connectionOptions; - this.currentConnection = createConnectionMono - .doOnSubscribe(c -> hasConnection.set(true)) - .cache(); + this.eventHubConnectionProcessor = createConnectionMono.subscribeWith(new EventHubConnectionProcessor()); } /** @@ -69,12 +67,12 @@ AmqpRetryOptions getRetryOptions() { * @return The Event Hub management node. */ Mono getManagementNode() { - return currentConnection.flatMap(EventHubAmqpConnection::getManagementNode); + return eventHubConnectionProcessor.flatMap(EventHubAmqpConnection::getManagementNode); } /** - * Creates or gets a send link. The same link is returned if there is an existing send link with the same - * {@code linkName}. Otherwise, a new link is created and returned. + * Creates or gets a send link. The same link is returned if there is an existing send link with the same {@code + * linkName}. Otherwise, a new link is created and returned. * * @param linkName The name of the link. * @param entityPath The remote address to connect to for the message broker. @@ -83,7 +81,7 @@ Mono getManagementNode() { * @return A new or existing send link that is connected to the given {@code entityPath}. */ Mono createSendLink(String linkName, String entityPath, AmqpRetryOptions retryOptions) { - return currentConnection.flatMap(connection -> connection.createSession(entityPath)) + return eventHubConnectionProcessor.flatMap(connection -> connection.createSession(entityPath)) .flatMap(session -> { logger.verbose("Creating producer for {}", entityPath); final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); @@ -106,7 +104,8 @@ Mono createSendLink(String linkName, String entityPath, AmqpRetryO */ Mono createReceiveLink(String linkName, String entityPath, EventPosition eventPosition, ReceiveOptions options) { - return currentConnection.flatMap(connection -> connection.createSession(entityPath).cast(EventHubSession.class)) + return eventHubConnectionProcessor + .flatMap(connection -> connection.createSession(entityPath).cast(EventHubSession.class)) .flatMap(session -> { logger.verbose("Creating consumer for path: {}", entityPath); final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(connectionOptions.getRetry()); @@ -127,10 +126,7 @@ public void close() { return; } - final AmqpConnection connection = currentConnection.block(connectionOptions.getRetry().getTryTimeout()); - if (connection != null) { - connection.close(); - } + eventHubConnectionProcessor.dispose(); if (connectionOptions.getScheduler() != null && !connectionOptions.getScheduler().isDisposed()) { connectionOptions.getScheduler().dispose(); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java new file mode 100644 index 000000000000..d2e19fadb684 --- /dev/null +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.azure.messaging.eventhubs.implementation; + +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.util.logging.ClientLogger; +import org.reactivestreams.Processor; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; + +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Subscribes to an upstream Mono that creates {@link EventHubAmqpConnection} then publishes the created connection + * until it closes then recreates it. + */ +public class EventHubConnectionProcessor extends Mono + implements Processor, CoreSubscriber, + Disposable { + + private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); + private final AtomicBoolean isTerminated = new AtomicBoolean(); + private final AtomicBoolean isDisposed = new AtomicBoolean(); + private final AtomicBoolean isRequested = new AtomicBoolean(); + + private final Object lock = new Object(); + + private Subscription upstream; + + private volatile EventHubAmqpConnection currentConnection; + private volatile Disposable connectionSubscription; + private volatile Throwable lastError; + private ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); + + @Override + public void onSubscribe(Subscription subscription) { + this.upstream = subscription; + + // Don't request an EventHubAmqpConnection until there is a subscriber. + subscription.request(0); + } + + @Override + public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { + Objects.requireNonNull(eventHubAmqpConnection, "'eventHubAmqpConnection' cannot be null."); + + final EventHubAmqpConnection oldConnection; + final Disposable oldSubscription; + synchronized (lock) { + oldConnection = currentConnection; + oldSubscription = connectionSubscription; + + currentConnection = eventHubAmqpConnection; + + final ConcurrentLinkedDeque current = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + current.forEach(subscriber -> subscriber.complete(eventHubAmqpConnection)); + + connectionSubscription = eventHubAmqpConnection.getEndpointStates().subscribe( + state -> { + }, + error -> onError(error), + () -> { + if (isDisposed()) { + logger.info("Connection is disposed. Not requesting another connection."); + } else { + logger.info("Connection closed. Requesting another."); + upstream.request(1); + } + }); + } + + if (oldConnection != null) { + oldConnection.close(); + } + + if (oldSubscription != null) { + oldSubscription.dispose(); + } + + isRequested.set(false); + } + + @Override + public void onError(Throwable throwable) { + if (throwable instanceof AmqpException && ((AmqpException) throwable).isTransient()) { + logger.warning("Transient occurred in connection. Retrying. Error: {}", throwable); + upstream.request(1); + return; + } + + logger.warning("Non-retryable error occurred in connection. Error: {}", throwable); + lastError = throwable; + isTerminated.set(true); + + synchronized (lock) { + final ConcurrentLinkedDeque current = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + current.forEach(subscriber -> subscriber.onError(throwable)); + } + } + + @Override + public void onComplete() { + logger.info("Completing EventHubConnectionSubscriber."); + + isTerminated.set(true); + synchronized (lock) { + final ConcurrentLinkedDeque current = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + current.forEach(subscriber -> subscriber.onComplete()); + } + } + + @Override + public void subscribe(CoreSubscriber actual) { + final ConnectionSubscriber subscriber = new ConnectionSubscriber(actual, this); + actual.onSubscribe(subscriber); + + if (isTerminated.get()) { + if (lastError != null) { + subscriber.onError(lastError); + } else { + subscriber.onComplete(); + } + + return; + } + + synchronized (lock) { + if (currentConnection != null) { + logger.info("Existing connection exists. Exposing that one."); + subscriber.complete(currentConnection); + return; + } + + subscribers.add(subscriber); + + if (!isRequested.getAndSet(true)) { + logger.info("Connection not obtained yet. Requesting one."); + upstream.request(1); + } + } + } + + @Override + public void dispose() { + if (isDisposed.getAndSet(true)) { + return; + } + + synchronized (lock) { + if (currentConnection != null) { + currentConnection.close(); + } + + currentConnection = null; + } + } + + @Override + public boolean isDisposed() { + return isDisposed.get(); + } + + private static class ConnectionSubscriber + extends Operators.MonoSubscriber { + private final EventHubConnectionProcessor processor; + + private ConnectionSubscriber(CoreSubscriber actual, + EventHubConnectionProcessor processor) { + super(actual); + this.processor = processor; + } + + @Override + public void cancel() { + super.cancel(); + processor.subscribers.remove(this); + } + + @Override + public void onComplete() { + if (!isCancelled()) { + actual.onComplete(); + } + } + + @Override + public void onNext(EventHubAmqpConnection connection) { + if (!isCancelled()) { + super.onNext(connection); + actual.onNext(connection); + } + } + + @Override + public void onError(Throwable throwable) { + if (!isCancelled()) { + actual.onError(throwable); + } else { + Operators.onOperatorError(throwable, currentContext()); + } + } + } +} From b36d9f63763721505199502eb59b80a226415bb1 Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 2 Dec 2019 00:48:07 -0800 Subject: [PATCH 02/31] Adding tests. --- .../EventHubConnectionProcessorTest.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java new file mode 100644 index 000000000000..b67ff0e34d65 --- /dev/null +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.azure.messaging.eventhubs.implementation; + +import com.azure.core.amqp.AmqpEndpointState; +import com.azure.core.amqp.AmqpShutdownSignal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.DirectProcessor; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; + +import static org.mockito.Mockito.when; + +/** + * Tests for {@link EventHubConnectionProcessor}. + */ +public class EventHubConnectionProcessorTest { + @Mock + private EventHubAmqpConnection connection; + @Mock + private EventHubAmqpConnection connection2; + @Mock + private EventHubAmqpConnection connection3; + + private final Duration timeout = Duration.ofSeconds(10); + private DirectProcessor endpointProcessor = DirectProcessor.create(); + private DirectProcessor shutdownSignalProcessor = DirectProcessor.create(); + + private EventHubConnectionProcessor eventHubConnectionProcessor = new EventHubConnectionProcessor(); + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + when(connection.getShutdownSignals()).thenReturn(shutdownSignalProcessor); + } + + /** + * Verifies that we can get a new connection. + */ + @Test + public void createsNewConnection() { + EventHubConnectionProcessor processor = Mono.fromCallable(() -> connection).repeat() + .subscribeWith(eventHubConnectionProcessor); + + StepVerifier.create(processor) + .expectNext(connection) + .expectComplete() + .verify(timeout); + } + + + /** + * Verifies that we can get a new connection. + */ + @Test + public void sameConnectionReturned() { + EventHubConnectionProcessor processor = Mono.fromCallable(() -> connection).repeat() + .subscribeWith(eventHubConnectionProcessor); + + StepVerifier.create(processor) + .expectNext(connection) + .expectComplete() + .verify(timeout); + + System.out.println("Logger."); + StepVerifier.create(processor) + .expectNext(connection) + .expectComplete() + .verify(timeout); + } +} From 8c9bfbdc265d9d82da19e002c2ca0760fe617ddd Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 2 Dec 2019 00:53:27 -0800 Subject: [PATCH 03/31] Removing unneeded local variable assignment. --- .../EventHubConnectionProcessor.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index d2e19fadb684..0b8ad6851e43 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -39,17 +39,16 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); - private final AtomicBoolean isDisposed = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); private final Object lock = new Object(); private Subscription upstream; + private ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); private volatile EventHubAmqpConnection currentConnection; private volatile Disposable connectionSubscription; private volatile Throwable lastError; - private ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); @Override public void onSubscribe(Subscription subscription) { @@ -71,11 +70,9 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { currentConnection = eventHubAmqpConnection; - final ConcurrentLinkedDeque current = subscribers; + subscribers.forEach(subscriber -> subscriber.complete(eventHubAmqpConnection)); subscribers = new ConcurrentLinkedDeque<>(); - current.forEach(subscriber -> subscriber.complete(eventHubAmqpConnection)); - connectionSubscription = eventHubAmqpConnection.getEndpointStates().subscribe( state -> { }, @@ -112,12 +109,11 @@ public void onError(Throwable throwable) { logger.warning("Non-retryable error occurred in connection. Error: {}", throwable); lastError = throwable; isTerminated.set(true); + dispose(); synchronized (lock) { - final ConcurrentLinkedDeque current = subscribers; + subscribers.forEach(subscriber -> subscriber.onError(throwable)); subscribers = new ConcurrentLinkedDeque<>(); - - current.forEach(subscriber -> subscriber.onError(throwable)); } } @@ -127,10 +123,8 @@ public void onComplete() { isTerminated.set(true); synchronized (lock) { - final ConcurrentLinkedDeque current = subscribers; + subscribers.forEach(subscriber -> subscriber.onComplete()); subscribers = new ConcurrentLinkedDeque<>(); - - current.forEach(subscriber -> subscriber.onComplete()); } } @@ -167,10 +161,6 @@ public void subscribe(CoreSubscriber actual) { @Override public void dispose() { - if (isDisposed.getAndSet(true)) { - return; - } - synchronized (lock) { if (currentConnection != null) { currentConnection.close(); @@ -182,9 +172,12 @@ public void dispose() { @Override public boolean isDisposed() { - return isDisposed.get(); + return isTerminated.get(); } + /** + * Represents a subscriber, waiting for an AMQP connection. + */ private static class ConnectionSubscriber extends Operators.MonoSubscriber { private final EventHubConnectionProcessor processor; From 66e9fca047c2f8c8d8da93a7f66392af9eede4ba Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 2 Dec 2019 09:41:36 -0800 Subject: [PATCH 04/31] Add tests for fetching new connection. --- .../EventHubConnectionProcessor.java | 5 +- .../EventHubConnectionProcessorTest.java | 85 ++++++++++++++++++- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 0b8ad6851e43..e305e143f018 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -40,14 +40,13 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); - private final Object lock = new Object(); private Subscription upstream; private ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); + private EventHubAmqpConnection currentConnection; + private Disposable connectionSubscription; - private volatile EventHubAmqpConnection currentConnection; - private volatile Disposable connectionSubscription; private volatile Throwable lastError; @Override diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index b67ff0e34d65..076bda46c6c4 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -23,10 +23,13 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import reactor.core.publisher.DirectProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; import static org.mockito.Mockito.when; @@ -69,24 +72,100 @@ public void createsNewConnection() { .verify(timeout); } - /** - * Verifies that we can get a new connection. + * Verifies that we can get the same, open connection when subscribing twice. */ @Test public void sameConnectionReturned() { + // Arrange EventHubConnectionProcessor processor = Mono.fromCallable(() -> connection).repeat() .subscribeWith(eventHubConnectionProcessor); + // Act & Assert StepVerifier.create(processor) .expectNext(connection) .expectComplete() .verify(timeout); - System.out.println("Logger."); StepVerifier.create(processor) .expectNext(connection) .expectComplete() .verify(timeout); } + + /** + * Verifies that we can get the same, open connection when subscribing twice. + */ + @Test + public void newConnectionOnClose() { + // Arrange + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { + connection, + connection2, + connection3 + }; + + final Flux connectionsSink = createSink(connections); + final EventHubConnectionProcessor processor = connectionsSink.subscribeWith(eventHubConnectionProcessor); + final FluxSink endpointSink = endpointProcessor.sink(); + final DirectProcessor connection2EndpointProcessor = DirectProcessor.create(); + final FluxSink connection2Endpoint = connection2EndpointProcessor.sink(FluxSink.OverflowStrategy.BUFFER); + connection2Endpoint.next(AmqpEndpointState.ACTIVE); + + when(connection2.getEndpointStates()).thenReturn(connection2EndpointProcessor); + + // Act & Assert + + // Verify that we get the first connection. + StepVerifier.create(processor) + .then(() -> endpointSink.next(AmqpEndpointState.ACTIVE)) + .expectNext(connection) + .expectComplete() + .verify(timeout); + + // Close that connection. + endpointSink.complete(); + + // Expect that the next connection is returned to us. + StepVerifier.create(processor) + .expectNext(connection2) + .expectComplete() + .verify(timeout); + + // Close connection 2 + connection2Endpoint.complete(); + + // Expect that the new connection is returned again. + StepVerifier.create(processor) + .expectNext(connection3) + .expectComplete() + .verify(timeout); + + // Expect that the new connection is returned again. + StepVerifier.create(processor) + .expectNext(connection3) + .expectComplete() + .verify(timeout); + } + + private static Flux createSink(EventHubAmqpConnection[] connections) { + return Flux.create(emitter -> { + final AtomicInteger counter = new AtomicInteger(); + + emitter.onRequest(request -> { + for (int i = 0; i < request; i++) { + final int index = counter.getAndIncrement(); + + if (index == connections.length) { + emitter.error(new RuntimeException(String.format( + "Cannot emit more. Index: %s. # of Connections: %s", + index, connections.length))); + break; + } + + emitter.next(connections[index]); + } + }); + }, FluxSink.OverflowStrategy.BUFFER); + } } From e410ce187f560f59400415026fed4bb578160702 Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 30 Dec 2019 06:27:52 -0700 Subject: [PATCH 05/31] Replace ConcurrentLinkedDequeue with ArrayList. --- .../implementation/EventHubConnectionProcessor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index e305e143f018..24035209dd9c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -25,8 +25,8 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import java.util.ArrayList; import java.util.Objects; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -40,10 +40,10 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); + private final ArrayList subscribers = new ArrayList<>(); private final Object lock = new Object(); private Subscription upstream; - private ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); private EventHubAmqpConnection currentConnection; private Disposable connectionSubscription; @@ -70,7 +70,7 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { currentConnection = eventHubAmqpConnection; subscribers.forEach(subscriber -> subscriber.complete(eventHubAmqpConnection)); - subscribers = new ConcurrentLinkedDeque<>(); + subscribers.clear(); connectionSubscription = eventHubAmqpConnection.getEndpointStates().subscribe( state -> { @@ -112,7 +112,7 @@ public void onError(Throwable throwable) { synchronized (lock) { subscribers.forEach(subscriber -> subscriber.onError(throwable)); - subscribers = new ConcurrentLinkedDeque<>(); + subscribers.clear(); } } @@ -123,7 +123,7 @@ public void onComplete() { isTerminated.set(true); synchronized (lock) { subscribers.forEach(subscriber -> subscriber.onComplete()); - subscribers = new ConcurrentLinkedDeque<>(); + subscribers.clear(); } } From 64ba300756ae12f07d28760a97191cfe32fdbeb3 Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 30 Dec 2019 08:47:13 -0700 Subject: [PATCH 06/31] Add tests to ensure on error conditions. --- .../EventHubConnectionProcessorTest.java | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index 076bda46c6c4..e0ac0669392e 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -18,6 +18,9 @@ import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpShutdownSignal; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpErrorContext; +import com.azure.core.amqp.exception.AmqpException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -29,6 +32,7 @@ import reactor.test.StepVerifier; import java.time.Duration; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import static org.mockito.Mockito.when; @@ -94,7 +98,7 @@ public void sameConnectionReturned() { } /** - * Verifies that we can get the same, open connection when subscribing twice. + * Verifies that we can get the next connection when the first one is closed. */ @Test public void newConnectionOnClose() { @@ -148,6 +152,84 @@ public void newConnectionOnClose() { .verify(timeout); } + /** + * Verifies that we can get the next connection when the first one encounters a retryable error. + */ + @Test + public void newConnectionOnRetryableError() { + // Arrange + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { + connection, + connection2 + }; + final AmqpException amqpException = new AmqpException(true, AmqpErrorCondition.SERVER_BUSY_ERROR, "Test-error", + new AmqpErrorContext("test-namespace")); + + final Flux connectionsSink = createSink(connections); + final EventHubConnectionProcessor processor = connectionsSink.subscribeWith(eventHubConnectionProcessor); + final FluxSink endpointSink = endpointProcessor.sink(); + + // Act & Assert + // Verify that we get the first connection. + StepVerifier.create(processor) + .then(() -> endpointSink.next(AmqpEndpointState.ACTIVE)) + .expectNext(connection) + .expectComplete() + .verify(timeout); + + endpointSink.error(amqpException); + + // Expect that the next connection is returned to us. + StepVerifier.create(processor) + .expectNext(connection2) + .expectComplete() + .verify(timeout); + + // Expect that the next connection is returned to us. + StepVerifier.create(processor) + .expectNext(connection2) + .expectComplete() + .verify(timeout); + } + + /** + * Verifies that an error is propagated when the first connection encounters a non-retryable error. + */ + @Test + public void nonRetryableError() { + // Arrange + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { + connection, + connection2 + }; + final AmqpException amqpException = new AmqpException(false, AmqpErrorCondition.ILLEGAL_STATE, "Test-error", + new AmqpErrorContext("test-namespace")); + + final Flux connectionsSink = createSink(connections); + final EventHubConnectionProcessor processor = connectionsSink.subscribeWith(eventHubConnectionProcessor); + final FluxSink endpointSink = endpointProcessor.sink(); + + // Act & Assert + // Verify that we get the first connection. + StepVerifier.create(processor) + .then(() -> endpointSink.next(AmqpEndpointState.ACTIVE)) + .expectNext(connection) + .expectComplete() + .verify(timeout); + + endpointSink.error(amqpException); + + // Expect that the error is returned to us. + StepVerifier.create(processor) + .expectErrorMatches(error -> Objects.equals(amqpException, error)) + .verify(timeout); + + // Expect that the error is returned to us again. + StepVerifier.create(processor) + .expectErrorMatches(error -> Objects.equals(amqpException, error)) + .verify(timeout); + } + private static Flux createSink(EventHubAmqpConnection[] connections) { return Flux.create(emitter -> { final AtomicInteger counter = new AtomicInteger(); From 721d33e1c4648c8b5c18b80e6309443b571cd687 Mon Sep 17 00:00:00 2001 From: Connie Date: Wed, 1 Jan 2020 17:38:20 -0800 Subject: [PATCH 07/31] =?UTF-8?q?Require=20not=20null=20for=20EventConnect?= =?UTF-8?q?ion=20subscribe=20methods=CB=86.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventHubConnectionProcessor.java | 4 ++ .../EventHubConnectionProcessorTest.java | 71 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 24035209dd9c..16a8a7c07cd6 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -99,6 +99,8 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { @Override public void onError(Throwable throwable) { + Objects.requireNonNull(throwable, "'throwable' is required."); + if (throwable instanceof AmqpException && ((AmqpException) throwable).isTransient()) { logger.warning("Transient occurred in connection. Retrying. Error: {}", throwable); upstream.request(1); @@ -160,6 +162,8 @@ public void subscribe(CoreSubscriber actual) { @Override public void dispose() { + onComplete(); + synchronized (lock) { if (currentConnection != null) { currentConnection.close(); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index e0ac0669392e..d2c3c7e2103e 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -21,10 +21,12 @@ import com.azure.core.amqp.exception.AmqpErrorCondition; import com.azure.core.amqp.exception.AmqpErrorContext; import com.azure.core.amqp.exception.AmqpException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.reactivestreams.Subscription; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -35,6 +37,9 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -103,10 +108,10 @@ public void sameConnectionReturned() { @Test public void newConnectionOnClose() { // Arrange - final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { - connection, - connection2, - connection3 + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ + connection, + connection2, + connection3 }; final Flux connectionsSink = createSink(connections); @@ -158,7 +163,7 @@ public void newConnectionOnClose() { @Test public void newConnectionOnRetryableError() { // Arrange - final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ connection, connection2 }; @@ -198,7 +203,7 @@ public void newConnectionOnRetryableError() { @Test public void nonRetryableError() { // Arrange - final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[] { + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ connection, connection2 }; @@ -230,6 +235,60 @@ public void nonRetryableError() { .verify(timeout); } + /** + * Verifies that when there are no subscribers, no request is fetched from upstream. + */ + @Test + public void noSubscribers() { + // Arrange + final Subscription subscription = mock(Subscription.class); + + // Act + eventHubConnectionProcessor.onSubscribe(subscription); + + // Assert + verify(subscription).request(eq(0L)); + } + + /** + * Verifies that when the processor has completed (ie. the instance is closed), no more connections are emitted. + */ + @Test + public void completesWhenTerminated() { + // Arrange + final EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ + connection, + }; + + final Flux connectionsSink = createSink(connections); + final EventHubConnectionProcessor processor = connectionsSink.subscribeWith(eventHubConnectionProcessor); + final FluxSink endpointSink = endpointProcessor.sink(); + + // Act & Assert + // Verify that we get the first connection. + StepVerifier.create(eventHubConnectionProcessor) + .then(() -> endpointSink.next(AmqpEndpointState.ACTIVE)) + .expectNext(connection) + .expectComplete() + .verify(timeout); + + eventHubConnectionProcessor.onComplete(); + + // Verify that it completes without emitting a connection. + StepVerifier.create(processor) + .expectComplete() + .verify(timeout); + } + + @Test + public void requiresNonNull() { + Assertions.assertThrows(NullPointerException.class, + () -> eventHubConnectionProcessor.onNext(null)); + + Assertions.assertThrows(NullPointerException.class, + () -> eventHubConnectionProcessor.onError(null)); + } + private static Flux createSink(EventHubAmqpConnection[] connections) { return Flux.create(emitter -> { final AtomicInteger counter = new AtomicInteger(); From 67cfe0f9809e6980cc84fc3f5de2d4f6726a0984 Mon Sep 17 00:00:00 2001 From: Connie Date: Wed, 1 Jan 2020 17:53:09 -0800 Subject: [PATCH 08/31] Remove circuar dependency on EventHubConnection from EventHubConnectionProcessor --- .../eventhubs/EventHubConnection.java | 34 ++++++++----------- .../EventHubConnectionProcessor.java | 33 ++++++++++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java index 3c4342d8d5c5..5f90abdb7e0e 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java @@ -12,7 +12,6 @@ import com.azure.core.amqp.implementation.RetryUtil; import com.azure.core.util.logging.ClientLogger; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; -import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; @@ -20,6 +19,7 @@ import reactor.core.publisher.Mono; import java.io.Closeable; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -28,15 +28,15 @@ class EventHubConnection implements Closeable { private final ClientLogger logger = new ClientLogger(EventHubConnection.class); private final AtomicBoolean hasConnection = new AtomicBoolean(); + private final EventHubAmqpConnection amqpConnection; private final ConnectionOptions connectionOptions; - private final EventHubConnectionProcessor eventHubConnectionProcessor; /** * Creates a new instance of {@link EventHubConnection}. */ - EventHubConnection(Mono createConnectionMono, ConnectionOptions connectionOptions) { - this.connectionOptions = connectionOptions; - this.eventHubConnectionProcessor = createConnectionMono.subscribeWith(new EventHubConnectionProcessor()); + EventHubConnection(EventHubAmqpConnection amqpConnection, ConnectionOptions connectionOptions) { + this.amqpConnection = Objects.requireNonNull(amqpConnection, "'amqpConnection' cannot be null."); + this.connectionOptions = Objects.requireNonNull(connectionOptions, "'connectionOptions' cannot be null."); } /** @@ -67,7 +67,7 @@ AmqpRetryOptions getRetryOptions() { * @return The Event Hub management node. */ Mono getManagementNode() { - return eventHubConnectionProcessor.flatMap(EventHubAmqpConnection::getManagementNode); + return amqpConnection.getManagementNode(); } /** @@ -77,18 +77,16 @@ Mono getManagementNode() { * @param linkName The name of the link. * @param entityPath The remote address to connect to for the message broker. * @param retryOptions Options to use when creating the link. - * * @return A new or existing send link that is connected to the given {@code entityPath}. */ Mono createSendLink(String linkName, String entityPath, AmqpRetryOptions retryOptions) { - return eventHubConnectionProcessor.flatMap(connection -> connection.createSession(entityPath)) - .flatMap(session -> { - logger.verbose("Creating producer for {}", entityPath); - final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); + return amqpConnection.createSession(entityPath).flatMap(session -> { + logger.verbose("Creating producer for {}", entityPath); + final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); - return session.createProducer(linkName, entityPath, retryOptions.getTryTimeout(), retryPolicy) - .cast(AmqpSendLink.class); - }); + return session.createProducer(linkName, entityPath, retryOptions.getTryTimeout(), retryPolicy) + .cast(AmqpSendLink.class); + }); } /** @@ -99,13 +97,11 @@ Mono createSendLink(String linkName, String entityPath, AmqpRetryO * @param entityPath The remote address to connect to for the message broker. * @param eventPosition Position to set the receive link to. * @param options Consumer options to use when creating the link. - * * @return A new or existing receive link that is connected to the given {@code entityPath}. */ Mono createReceiveLink(String linkName, String entityPath, EventPosition eventPosition, - ReceiveOptions options) { - return eventHubConnectionProcessor - .flatMap(connection -> connection.createSession(entityPath).cast(EventHubSession.class)) + ReceiveOptions options) { + return amqpConnection.createSession(entityPath).cast(EventHubSession.class) .flatMap(session -> { logger.verbose("Creating consumer for path: {}", entityPath); final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(connectionOptions.getRetry()); @@ -126,7 +122,7 @@ public void close() { return; } - eventHubConnectionProcessor.dispose(); + amqpConnection.close(); if (connectionOptions.getScheduler() != null && !connectionOptions.getScheduler().isDisposed()) { connectionOptions.getScheduler().dispose(); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 16a8a7c07cd6..22e010b09b12 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -16,6 +16,7 @@ package com.azure.messaging.eventhubs.implementation; +import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.exception.AmqpException; import com.azure.core.util.logging.ClientLogger; import org.reactivestreams.Processor; @@ -42,6 +43,9 @@ public class EventHubConnectionProcessor extends Mono private final AtomicBoolean isRequested = new AtomicBoolean(); private final ArrayList subscribers = new ArrayList<>(); private final Object lock = new Object(); + private final String fullyQualifiedNamespace; + private final String eventHubName; + private final AmqpRetryOptions retryOptions; private Subscription upstream; private EventHubAmqpConnection currentConnection; @@ -49,6 +53,35 @@ public class EventHubConnectionProcessor extends Mono private volatile Throwable lastError; + EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventHubName, AmqpRetryOptions retryOptions) { + this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, + "'fullyQualifiedNamespace' cannot be null."); + this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null."); + this.retryOptions = Objects.requireNonNull(retryOptions, "'retryOptions' cannot be null."); + } + + /** + * Gets the fully qualified namespace for the connection. + * + * @return The fully qualified namespace this is connection. + */ + public String getFullyQualifiedNamespace() { + return fullyQualifiedNamespace; + } + + /** + * Gets the name of the Event Hub. + * + * @return The name of the Event Hub. + */ + public String getEventHubName() { + return eventHubName; + } + + AmqpRetryOptions getRetryOptions() { + return retryOptions; + } + @Override public void onSubscribe(Subscription subscription) { this.upstream = subscription; From 0214e1ae944db496f7ec502f8101e86f85fdc055 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:54:22 -0800 Subject: [PATCH 09/31] Removing EventHubConnection. --- .../eventhubs/EventHubConnection.java | 131 --------------- .../eventhubs/EventHubConnectionTest.java | 151 ------------------ 2 files changed, 282 deletions(-) delete mode 100644 sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java delete mode 100644 sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConnectionTest.java diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java deleted file mode 100644 index 5f90abdb7e0e..000000000000 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConnection.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.messaging.eventhubs; - -import com.azure.core.amqp.AmqpRetryOptions; -import com.azure.core.amqp.AmqpRetryPolicy; -import com.azure.core.amqp.exception.AmqpException; -import com.azure.core.amqp.implementation.AmqpReceiveLink; -import com.azure.core.amqp.implementation.AmqpSendLink; -import com.azure.core.amqp.implementation.ConnectionOptions; -import com.azure.core.amqp.implementation.RetryUtil; -import com.azure.core.util.logging.ClientLogger; -import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; -import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; -import com.azure.messaging.eventhubs.implementation.EventHubSession; -import com.azure.messaging.eventhubs.models.EventPosition; -import com.azure.messaging.eventhubs.models.ReceiveOptions; -import reactor.core.publisher.Mono; - -import java.io.Closeable; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Class that manages the connection to Azure Event Hubs. - */ -class EventHubConnection implements Closeable { - private final ClientLogger logger = new ClientLogger(EventHubConnection.class); - private final AtomicBoolean hasConnection = new AtomicBoolean(); - private final EventHubAmqpConnection amqpConnection; - private final ConnectionOptions connectionOptions; - - /** - * Creates a new instance of {@link EventHubConnection}. - */ - EventHubConnection(EventHubAmqpConnection amqpConnection, ConnectionOptions connectionOptions) { - this.amqpConnection = Objects.requireNonNull(amqpConnection, "'amqpConnection' cannot be null."); - this.connectionOptions = Objects.requireNonNull(connectionOptions, "'connectionOptions' cannot be null."); - } - - /** - * Gets the fully qualified namespace for the connection. - * - * @return The fully qualified namespace this is connection. - */ - public String getFullyQualifiedNamespace() { - return connectionOptions.getFullyQualifiedNamespace(); - } - - /** - * Gets the name of the Event Hub. - * - * @return The name of the Event Hub. - */ - public String getEventHubName() { - return connectionOptions.getEntityPath(); - } - - AmqpRetryOptions getRetryOptions() { - return connectionOptions.getRetry(); - } - - /** - * Gets the Event Hub management node. - * - * @return The Event Hub management node. - */ - Mono getManagementNode() { - return amqpConnection.getManagementNode(); - } - - /** - * Creates or gets a send link. The same link is returned if there is an existing send link with the same {@code - * linkName}. Otherwise, a new link is created and returned. - * - * @param linkName The name of the link. - * @param entityPath The remote address to connect to for the message broker. - * @param retryOptions Options to use when creating the link. - * @return A new or existing send link that is connected to the given {@code entityPath}. - */ - Mono createSendLink(String linkName, String entityPath, AmqpRetryOptions retryOptions) { - return amqpConnection.createSession(entityPath).flatMap(session -> { - logger.verbose("Creating producer for {}", entityPath); - final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); - - return session.createProducer(linkName, entityPath, retryOptions.getTryTimeout(), retryPolicy) - .cast(AmqpSendLink.class); - }); - } - - /** - * Creates or gets an existing receive link. The same link is returned if there is an existing receive link with the - * same {@code linkName}. Otherwise, a new link is created and returned. - * - * @param linkName The name of the link. - * @param entityPath The remote address to connect to for the message broker. - * @param eventPosition Position to set the receive link to. - * @param options Consumer options to use when creating the link. - * @return A new or existing receive link that is connected to the given {@code entityPath}. - */ - Mono createReceiveLink(String linkName, String entityPath, EventPosition eventPosition, - ReceiveOptions options) { - return amqpConnection.createSession(entityPath).cast(EventHubSession.class) - .flatMap(session -> { - logger.verbose("Creating consumer for path: {}", entityPath); - final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(connectionOptions.getRetry()); - - return session.createConsumer(linkName, entityPath, connectionOptions.getRetry().getTryTimeout(), - retryPolicy, eventPosition, options); - }); - } - - /** - * Disposes of the Event Hub connection. - * - * @throws AmqpException if the connection encountered an exception while closing. - */ - @Override - public void close() { - if (!hasConnection.getAndSet(false)) { - return; - } - - amqpConnection.close(); - - if (connectionOptions.getScheduler() != null && !connectionOptions.getScheduler().isDisposed()) { - connectionOptions.getScheduler().dispose(); - } - } -} diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConnectionTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConnectionTest.java deleted file mode 100644 index cb21b35b7b4d..000000000000 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConnectionTest.java +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.messaging.eventhubs; - -import com.azure.core.amqp.AmqpRetryMode; -import com.azure.core.amqp.AmqpRetryOptions; -import com.azure.core.amqp.AmqpTransportType; -import com.azure.core.amqp.ExponentialAmqpRetryPolicy; -import com.azure.core.amqp.FixedAmqpRetryPolicy; -import com.azure.core.amqp.ProxyOptions; -import com.azure.core.amqp.implementation.AmqpReceiveLink; -import com.azure.core.amqp.implementation.AmqpSendLink; -import com.azure.core.amqp.implementation.CbsAuthorizationType; -import com.azure.core.amqp.implementation.ConnectionOptions; -import com.azure.core.credential.TokenCredential; -import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; -import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; -import com.azure.messaging.eventhubs.implementation.EventHubSession; -import com.azure.messaging.eventhubs.models.EventPosition; -import com.azure.messaging.eventhubs.models.ReceiveOptions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; - -import java.io.IOException; -import java.time.Duration; - -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class EventHubConnectionTest { - private static final Duration TIMEOUT = Duration.ofSeconds(2); - private static final String HOST_NAME = "Some-host-name"; - private final AmqpRetryOptions retryOptions = new AmqpRetryOptions() - .setTryTimeout(Duration.ofSeconds(5)) - .setMaxRetries(0); - - @Mock - private EventHubAmqpConnection connection; - @Mock - private EventHubSession session; - @Mock - private TokenCredential tokenCredential; - - private EventHubConnection provider; - - @BeforeEach - public void setup() { - MockitoAnnotations.initMocks(this); - ConnectionOptions connectionOptions = new ConnectionOptions(HOST_NAME, "event-hub-path", tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - provider = new EventHubConnection(Mono.just(connection), connectionOptions); - } - - @Test - public void getManagementNode() { - // Arrange - EventHubManagementNode managementNode = mock(EventHubManagementNode.class); - when(connection.getManagementNode()).thenReturn(Mono.just(managementNode)); - - // Act & Assert - StepVerifier.create(provider.getManagementNode()) - .expectNext(managementNode) - .expectComplete() - .verify(TIMEOUT); - } - - @Test - public void getSendLink() { - // Arrange - final Duration timeout = Duration.ofSeconds(4); - final AmqpSendLink sendLink = mock(AmqpSendLink.class); - final AmqpRetryOptions options = new AmqpRetryOptions() - .setTryTimeout(timeout) - .setMaxRetries(2) - .setMode(AmqpRetryMode.FIXED); - - final String linkName = "some-link-name"; - final String entityPath = "some-entity-path"; - when(connection.createSession(entityPath)).thenReturn(Mono.just(session)); - when(session.createProducer(eq(linkName), eq(entityPath), eq(timeout), - argThat(matcher -> options.getMaxRetries() == matcher.getMaxRetries() - && matcher instanceof FixedAmqpRetryPolicy))) - .thenReturn(Mono.just(sendLink)); - - // Act & Assert - StepVerifier.create(provider.createSendLink(linkName, entityPath, options)) - .expectNext(sendLink) - .verifyComplete(); - } - - @Test - public void getReceiveLink() { - // Arrange - final AmqpReceiveLink receiveLink = mock(AmqpReceiveLink.class); - final ReceiveOptions options = new ReceiveOptions(); - - final EventPosition position = EventPosition.fromOffset(10L); - final String linkName = "some-link-name"; - final String entityPath = "some-entity-path"; - - when(connection.createSession(entityPath)).thenReturn(Mono.just(session)); - when(session.createConsumer( - eq(linkName), eq(entityPath), eq(retryOptions.getTryTimeout()), - argThat(matcher -> retryOptions.getMaxRetries() == matcher.getMaxRetries() - && matcher instanceof ExponentialAmqpRetryPolicy), - eq(position), eq(options))) - .thenReturn(Mono.just(receiveLink)); - - // Act & Assert - StepVerifier.create(provider.createReceiveLink(linkName, entityPath, position, options)) - .expectNext(receiveLink) - .verifyComplete(); - } - - @Test - public void disposesOnce() throws IOException { - // Arrange - final EventHubManagementNode node = mock(EventHubManagementNode.class); - when(connection.getManagementNode()).thenReturn(Mono.just(node)); - - // Force us to evaluate the connection Mono - provider.getManagementNode().block(); - - // Act - provider.close(); - - // This should not call connection.close() a second time. The connection has already been disposed. - provider.close(); - - // Assert - verify(connection, times(1)).close(); - } - - @AfterEach - public void teardown() { - Mockito.framework().clearInlineMocks(); - } -} From a3a00308e7da2ed46b1e84da74c80d7e735ca5cc Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:40:55 -0800 Subject: [PATCH 10/31] Update AmqpConnection interface to create send and receive links. --- .../EventHubAmqpConnection.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubAmqpConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubAmqpConnection.java index efa2ed2303f9..e992419c41ba 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubAmqpConnection.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubAmqpConnection.java @@ -4,6 +4,11 @@ package com.azure.messaging.eventhubs.implementation; import com.azure.core.amqp.AmqpConnection; +import com.azure.core.amqp.AmqpRetryOptions; +import com.azure.core.amqp.implementation.AmqpReceiveLink; +import com.azure.core.amqp.implementation.AmqpSendLink; +import com.azure.messaging.eventhubs.models.EventPosition; +import com.azure.messaging.eventhubs.models.ReceiveOptions; import reactor.core.publisher.Mono; /** @@ -16,4 +21,28 @@ public interface EventHubAmqpConnection extends AmqpConnection { * @return A Mono that completes with a session to the Event Hub's management node. */ Mono getManagementNode(); + + /** + * Creates or gets a send link. The same link is returned if there is an existing send link with the same {@code + * linkName}. Otherwise, a new link is created and returned. + * + * @param linkName The name of the link. + * @param entityPath The remote address to connect to for the message broker. + * @param retryOptions Options to use when creating the link. + * @return A new or existing send link that is connected to the given {@code entityPath}. + */ + Mono createSendLink(String linkName, String entityPath, AmqpRetryOptions retryOptions); + + /** + * Creates or gets an existing receive link. The same link is returned if there is an existing receive link with the + * same {@code linkName}. Otherwise, a new link is created and returned. + * + * @param linkName The name of the link. + * @param entityPath The remote address to connect to for the message broker. + * @param eventPosition Position to set the receive link to. + * @param options Consumer options to use when creating the link. + * @return A new or existing receive link that is connected to the given {@code entityPath}. + */ + Mono createReceiveLink(String linkName, String entityPath, EventPosition eventPosition, + ReceiveOptions options); } From aaa362633dc608284d1921edc993e28d16af4b0c Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:42:10 -0800 Subject: [PATCH 11/31] Fixing errors in ConnectionProcessor and update ReactorAmqpConnection to keep track of send links. --- .../EventHubConnectionProcessor.java | 33 +++++--- .../EventHubReactorAmqpConnection.java | 78 ++++++++++++++++++- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 22e010b09b12..9f477651b11a 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -26,8 +26,8 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; -import java.util.ArrayList; import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -41,7 +41,6 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); - private final ArrayList subscribers = new ArrayList<>(); private final Object lock = new Object(); private final String fullyQualifiedNamespace; private final String eventHubName; @@ -51,9 +50,11 @@ public class EventHubConnectionProcessor extends Mono private EventHubAmqpConnection currentConnection; private Disposable connectionSubscription; + private volatile ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); private volatile Throwable lastError; - EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventHubName, AmqpRetryOptions retryOptions) { + public EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventHubName, + AmqpRetryOptions retryOptions) { this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, "'fullyQualifiedNamespace' cannot be null."); this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null."); @@ -78,7 +79,7 @@ public String getEventHubName() { return eventHubName; } - AmqpRetryOptions getRetryOptions() { + public AmqpRetryOptions getRetryOptions() { return retryOptions; } @@ -92,6 +93,8 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { + logger.info("Setting next AMQP connection."); + Objects.requireNonNull(eventHubAmqpConnection, "'eventHubAmqpConnection' cannot be null."); final EventHubAmqpConnection oldConnection; @@ -102,8 +105,10 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { currentConnection = eventHubAmqpConnection; - subscribers.forEach(subscriber -> subscriber.complete(eventHubAmqpConnection)); - subscribers.clear(); + final ConcurrentLinkedDeque currentSubscribers = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + currentSubscribers.forEach(subscription -> subscription.onNext(eventHubAmqpConnection)); connectionSubscription = eventHubAmqpConnection.getEndpointStates().subscribe( state -> { @@ -135,7 +140,7 @@ public void onError(Throwable throwable) { Objects.requireNonNull(throwable, "'throwable' is required."); if (throwable instanceof AmqpException && ((AmqpException) throwable).isTransient()) { - logger.warning("Transient occurred in connection. Retrying. Error: {}", throwable); + logger.warning("Transient occurred in connection. Retrying.", throwable); upstream.request(1); return; } @@ -146,8 +151,10 @@ public void onError(Throwable throwable) { dispose(); synchronized (lock) { - subscribers.forEach(subscriber -> subscriber.onError(throwable)); - subscribers.clear(); + final ConcurrentLinkedDeque currentSubscribers = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + currentSubscribers.forEach(subscriber -> subscriber.onError(throwable)); } } @@ -157,13 +164,17 @@ public void onComplete() { isTerminated.set(true); synchronized (lock) { - subscribers.forEach(subscriber -> subscriber.onComplete()); - subscribers.clear(); + final ConcurrentLinkedDeque currentSubscribers = subscribers; + subscribers = new ConcurrentLinkedDeque<>(); + + currentSubscribers.forEach(subscriber -> subscriber.onComplete()); } } @Override public void subscribe(CoreSubscriber actual) { + logger.info("Subscription received."); + final ConnectionSubscriber subscriber = new ConnectionSubscriber(actual, this); actual.onSubscribe(subscriber); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java index f02043178245..178db62ee1a5 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java @@ -4,18 +4,27 @@ package com.azure.messaging.eventhubs.implementation; import com.azure.core.amqp.AmqpRetryOptions; +import com.azure.core.amqp.AmqpRetryPolicy; import com.azure.core.amqp.AmqpSession; +import com.azure.core.amqp.implementation.AmqpReceiveLink; +import com.azure.core.amqp.implementation.AmqpSendLink; import com.azure.core.amqp.implementation.ConnectionOptions; import com.azure.core.amqp.implementation.MessageSerializer; import com.azure.core.amqp.implementation.ReactorConnection; import com.azure.core.amqp.implementation.ReactorHandlerProvider; import com.azure.core.amqp.implementation.ReactorProvider; +import com.azure.core.amqp.implementation.RetryUtil; import com.azure.core.amqp.implementation.TokenManagerProvider; import com.azure.core.amqp.implementation.handler.SessionHandler; +import com.azure.core.util.logging.ClientLogger; +import com.azure.messaging.eventhubs.models.EventPosition; +import com.azure.messaging.eventhubs.models.ReceiveOptions; import org.apache.qpid.proton.engine.BaseHandler; import org.apache.qpid.proton.engine.Session; import reactor.core.publisher.Mono; +import java.util.concurrent.ConcurrentHashMap; + /** * A proton-j AMQP connection to an Azure Event Hub instance. Adds additional support for management operations. */ @@ -24,6 +33,12 @@ public class EventHubReactorAmqpConnection extends ReactorConnection implements private static final String MANAGEMENT_LINK_NAME = "mgmt"; private static final String MANAGEMENT_ADDRESS = "$management"; + private final ClientLogger logger = new ClientLogger(EventHubReactorAmqpConnection.class); + /** + * Keeps track of the opened send links. Links are key'd by their entityPath. The send link for allowing the service + * load balance messages is the eventHubName. + */ + private final ConcurrentHashMap sendLinks = new ConcurrentHashMap<>(); private final Mono managementChannelMono; private final ReactorProvider reactorProvider; private final ReactorHandlerProvider handlerProvider; @@ -43,9 +58,8 @@ public class EventHubReactorAmqpConnection extends ReactorConnection implements */ public EventHubReactorAmqpConnection(String connectionId, ConnectionOptions connectionOptions, ReactorProvider reactorProvider, ReactorHandlerProvider handlerProvider, - TokenManagerProvider tokenManagerProvider, MessageSerializer messageSerializer, - String product, String clientVersion) { - + TokenManagerProvider tokenManagerProvider, MessageSerializer messageSerializer, String product, + String clientVersion) { super(connectionId, connectionOptions, reactorProvider, handlerProvider, tokenManagerProvider, messageSerializer, product, clientVersion); this.reactorProvider = reactorProvider; @@ -74,4 +88,62 @@ protected AmqpSession createSession(String sessionName, Session session, Session return new EventHubReactorSession(session, handler, sessionName, reactorProvider, handlerProvider, getClaimsBasedSecurityNode(), tokenManagerProvider, retryOptions.getTryTimeout(), messageSerializer); } + + /** + * Creates or gets a send link. The same link is returned if there is an existing send link with the same {@code + * linkName}. Otherwise, a new link is created and returned. + * + * @param linkName The name of the link. + * @param entityPath The remote address to connect to for the message broker. + * @param retryOptions Options to use when creating the link. + * @return A new or existing send link that is connected to the given {@code entityPath}. + */ + @Override + public Mono createSendLink(String linkName, String entityPath, AmqpRetryOptions retryOptions) { + final AmqpSendLink openLink = sendLinks.get(entityPath); + + if (openLink != null) { + return Mono.just(openLink); + } + + return createSession(entityPath).flatMap(session -> { + logger.info("Creating producer for {}", entityPath); + final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); + + return session.createProducer(linkName, entityPath, retryOptions.getTryTimeout(), retryPolicy) + .cast(AmqpSendLink.class); + }).map(link -> sendLinks.computeIfAbsent(entityPath, unusedKey -> link)); + } + + /** + * Creates or gets an existing receive link. The same link is returned if there is an existing receive link with the + * same {@code linkName}. Otherwise, a new link is created and returned. + * + * @param linkName The name of the link. + * @param entityPath The remote address to connect to for the message broker. + * @param eventPosition Position to set the receive link to. + * @param options Consumer options to use when creating the link. + * @return A new or existing receive link that is connected to the given {@code entityPath}. + */ + @Override + public Mono createReceiveLink(String linkName, String entityPath, EventPosition eventPosition, + ReceiveOptions options) { + return createSession(entityPath).cast(EventHubSession.class) + .flatMap(session -> { + logger.verbose("Creating consumer for path: {}", entityPath); + final AmqpRetryPolicy retryPolicy = RetryUtil.getRetryPolicy(retryOptions); + + return session.createConsumer(linkName, entityPath, retryOptions.getTryTimeout(), retryPolicy, + eventPosition, options); + }); + } + + @Override + public void close() { + logger.info("Disposing of connection."); + sendLinks.forEach((key, value) -> { value.close(); }); + sendLinks.clear(); + + super.close(); + } } From 603833fb568039439cbefda8f0469dd8865d9c76 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:42:56 -0800 Subject: [PATCH 12/31] Update clients to use ConnectionProcessor. --- .../eventhubs/EventHubAsyncClient.java | 31 +++++---- .../eventhubs/EventHubClientBuilder.java | 21 +++--- .../EventHubConsumerAsyncClient.java | 21 +++--- .../EventHubProducerAsyncClient.java | 68 +++++++------------ 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubAsyncClient.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubAsyncClient.java index 6015f6f7d9df..81e59731f888 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubAsyncClient.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubAsyncClient.java @@ -6,6 +6,7 @@ import com.azure.core.amqp.implementation.MessageSerializer; import com.azure.core.amqp.implementation.TracerProvider; import com.azure.core.util.logging.ClientLogger; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,15 +26,16 @@ class EventHubAsyncClient implements Closeable { private final ClientLogger logger = new ClientLogger(EventHubAsyncClient.class); private final MessageSerializer messageSerializer; - private final EventHubConnection connection; + private final EventHubConnectionProcessor connectionProcessor; private final boolean isSharedConnection; private final TracerProvider tracerProvider; - EventHubAsyncClient(EventHubConnection connection, TracerProvider tracerProvider, + EventHubAsyncClient(EventHubConnectionProcessor connectionProcessor, TracerProvider tracerProvider, MessageSerializer messageSerializer, boolean isSharedConnection) { this.tracerProvider = Objects.requireNonNull(tracerProvider, "'tracerProvider' cannot be null."); this.messageSerializer = Objects.requireNonNull(messageSerializer, "'messageSerializer' cannot be null."); - this.connection = Objects.requireNonNull(connection, "'connection' cannot be null."); + this.connectionProcessor = Objects.requireNonNull(connectionProcessor, + "'connectionProcessor' cannot be null."); this.isSharedConnection = isSharedConnection; } @@ -43,7 +45,7 @@ class EventHubAsyncClient implements Closeable { * @return The fully qualified namespace of this Event Hub. */ String getFullyQualifiedNamespace() { - return connection.getFullyQualifiedNamespace(); + return connectionProcessor.getFullyQualifiedNamespace(); } /** @@ -52,7 +54,7 @@ String getFullyQualifiedNamespace() { * @return The Event Hub name this client interacts with. */ String getEventHubName() { - return connection.getEventHubName(); + return connectionProcessor.getEventHubName(); } /** @@ -61,7 +63,9 @@ String getEventHubName() { * @return The set of information for the Event Hub that this client is associated with. */ Mono getProperties() { - return connection.getManagementNode().flatMap(EventHubManagementNode::getEventHubProperties); + return connectionProcessor + .flatMap(connection -> connection.getManagementNode()) + .flatMap(EventHubManagementNode::getEventHubProperties); } /** @@ -81,7 +85,9 @@ Flux getPartitionIds() { * @return The set of information for the requested partition under the Event Hub this client is associated with. */ Mono getPartitionProperties(String partitionId) { - return connection.getManagementNode().flatMap(node -> node.getPartitionProperties(partitionId)); + return connectionProcessor + .flatMap(connection -> connection.getManagementNode()) + .flatMap(node -> node.getPartitionProperties(partitionId)); } /** @@ -91,8 +97,9 @@ Mono getPartitionProperties(String partitionId) { * @return A new {@link EventHubProducerAsyncClient}. */ EventHubProducerAsyncClient createProducer() { - return new EventHubProducerAsyncClient(connection.getFullyQualifiedNamespace(), getEventHubName(), connection, - connection.getRetryOptions(), tracerProvider, messageSerializer, isSharedConnection); + return new EventHubProducerAsyncClient(connectionProcessor.getFullyQualifiedNamespace(), getEventHubName(), + connectionProcessor, connectionProcessor.getRetryOptions(), tracerProvider, messageSerializer, + isSharedConnection); } /** @@ -115,8 +122,8 @@ EventHubConsumerAsyncClient createConsumer(String consumerGroup, int prefetchCou new IllegalArgumentException("'consumerGroup' cannot be an empty string.")); } - return new EventHubConsumerAsyncClient(connection.getFullyQualifiedNamespace(), getEventHubName(), - connection, messageSerializer, consumerGroup, prefetchCount, isSharedConnection); + return new EventHubConsumerAsyncClient(connectionProcessor.getFullyQualifiedNamespace(), getEventHubName(), + connectionProcessor, messageSerializer, consumerGroup, prefetchCount, isSharedConnection); } /** @@ -126,6 +133,6 @@ EventHubConsumerAsyncClient createConsumer(String consumerGroup, int prefetchCou */ @Override public void close() { - connection.close(); + connectionProcessor.dispose(); } } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java index 9d7112ced1cf..fa88c34f2b96 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java @@ -26,6 +26,7 @@ import com.azure.core.util.tracing.Tracer; import com.azure.messaging.eventhubs.implementation.ClientConstants; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubReactorAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubSharedKeyCredential; import reactor.core.publisher.Mono; @@ -125,7 +126,7 @@ public class EventHubClientBuilder { private String fullyQualifiedNamespace; private String eventHubName; private String consumerGroup; - private EventHubConnection eventHubConnection; + private EventHubConnectionProcessor eventHubConnectionProcessor; private int prefetchCount; private boolean isSharedConnection; @@ -464,17 +465,17 @@ EventHubAsyncClient buildAsyncClient() { final MessageSerializer messageSerializer = new EventHubMessageSerializer(); - if (isSharedConnection && eventHubConnection == null) { - eventHubConnection = buildConnection(messageSerializer); + if (isSharedConnection && eventHubConnectionProcessor == null) { + eventHubConnectionProcessor = buildConnectionProcessor(messageSerializer); } - final EventHubConnection connection = isSharedConnection - ? eventHubConnection - : buildConnection(messageSerializer); + final EventHubConnectionProcessor processor = isSharedConnection + ? eventHubConnectionProcessor + : buildConnectionProcessor(messageSerializer); final TracerProvider tracerProvider = new TracerProvider(ServiceLoader.load(Tracer.class)); - return new EventHubAsyncClient(connection, tracerProvider, messageSerializer, isSharedConnection); + return new EventHubAsyncClient(processor, tracerProvider, messageSerializer, isSharedConnection); } /** @@ -508,7 +509,7 @@ EventHubClient buildClient() { return new EventHubClient(client, retryOptions); } - private EventHubConnection buildConnection(MessageSerializer messageSerializer) { + private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer messageSerializer) { final ConnectionOptions connectionOptions = getConnectionOptions(); final TokenManagerProvider tokenManagerProvider = new AzureTokenManagerProvider( connectionOptions.getAuthorizationType(), connectionOptions.getFullyQualifiedNamespace(), @@ -526,7 +527,9 @@ private EventHubConnection buildConnection(MessageSerializer messageSerializer) tokenManagerProvider, messageSerializer, product, clientVersion); }); - return new EventHubConnection(connectionMono, connectionOptions); + return connectionMono.subscribeWith(new EventHubConnectionProcessor( + connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), + connectionOptions.getRetry())); } private ConnectionOptions getConnectionOptions() { diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClient.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClient.java index 2dbd09b4b6c6..f71aa96a6327 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClient.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClient.java @@ -10,6 +10,7 @@ import com.azure.core.annotation.ServiceClient; import com.azure.core.annotation.ServiceMethod; import com.azure.core.util.logging.ClientLogger; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import com.azure.messaging.eventhubs.models.EventPosition; import com.azure.messaging.eventhubs.models.PartitionEvent; @@ -60,7 +61,7 @@ public class EventHubConsumerAsyncClient implements Closeable { private final ClientLogger logger = new ClientLogger(EventHubConsumerAsyncClient.class); private final String fullyQualifiedNamespace; private final String eventHubName; - private final EventHubConnection connection; + private final EventHubConnectionProcessor connectionProcessor; private final MessageSerializer messageSerializer; private final String consumerGroup; private final int prefetchCount; @@ -72,11 +73,12 @@ public class EventHubConsumerAsyncClient implements Closeable { private final ConcurrentHashMap openPartitionConsumers = new ConcurrentHashMap<>(); - EventHubConsumerAsyncClient(String fullyQualifiedNamespace, String eventHubName, EventHubConnection connection, - MessageSerializer messageSerializer, String consumerGroup, int prefetchCount, boolean isSharedConnection) { + EventHubConsumerAsyncClient(String fullyQualifiedNamespace, String eventHubName, + EventHubConnectionProcessor connectionProcessor, MessageSerializer messageSerializer, String consumerGroup, + int prefetchCount, boolean isSharedConnection) { this.fullyQualifiedNamespace = fullyQualifiedNamespace; this.eventHubName = eventHubName; - this.connection = connection; + this.connectionProcessor = connectionProcessor; this.messageSerializer = messageSerializer; this.consumerGroup = consumerGroup; this.prefetchCount = prefetchCount; @@ -118,7 +120,8 @@ public String getConsumerGroup() { */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono getEventHubProperties() { - return connection.getManagementNode().flatMap(EventHubManagementNode::getEventHubProperties); + return connectionProcessor.flatMap(connection -> connection.getManagementNode()) + .flatMap(EventHubManagementNode::getEventHubProperties); } /** @@ -148,7 +151,8 @@ public Mono getPartitionProperties(String partitionId) { return monoError(logger, new IllegalArgumentException("'partitionId' cannot be an empty string.")); } - return connection.getManagementNode().flatMap(node -> node.getPartitionProperties(partitionId)); + return connectionProcessor.flatMap(connection -> connection.getManagementNode()) + .flatMap(node -> node.getPartitionProperties(partitionId)); } /** @@ -304,7 +308,7 @@ public void close() { openPartitionConsumers.clear(); if (!isSharedConnection) { - connection.close(); + connectionProcessor.dispose(); } } @@ -333,7 +337,8 @@ private EventHubPartitionAsyncConsumer createPartitionConsumer(String linkName, getEventHubName(), consumerGroup, partitionId); final Mono receiveLinkMono = - connection.createReceiveLink(linkName, entityPath, startingPosition, receiveOptions) + connectionProcessor.flatMap(connection -> + connection.createReceiveLink(linkName, entityPath, startingPosition, receiveOptions)) .doOnNext(next -> logger.verbose("Creating consumer for path: {}", next.getEntityPath())); return new EventHubPartitionAsyncConsumer(receiveLinkMono, messageSerializer, getFullyQualifiedNamespace(), diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClient.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClient.java index 2c8f37fb2a4a..6f81a61d44dd 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClient.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClient.java @@ -19,6 +19,7 @@ import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; import com.azure.core.util.tracing.ProcessKind; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import com.azure.messaging.eventhubs.models.CreateBatchOptions; import com.azure.messaging.eventhubs.models.SendOptions; @@ -33,8 +34,8 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; @@ -51,8 +52,8 @@ /** * An asynchronous producer responsible for transmitting {@link EventData} to a specific Event Hub, grouped - * together in batches. Depending on the {@link CreateBatchOptions options} specified when creating an - * {@link EventDataBatch}, the events may be automatically routed to an available partition or specific to a partition. + * together in batches. Depending on the {@link CreateBatchOptions options} specified when creating an {@link + * EventDataBatch}, the events may be automatically routed to an available partition or specific to a partition. * *

* Allowing automatic routing of partitions is recommended when: @@ -94,16 +95,11 @@ public class EventHubProducerAsyncClient implements Closeable { private static final SendOptions DEFAULT_SEND_OPTIONS = new SendOptions(); private static final CreateBatchOptions DEFAULT_BATCH_OPTIONS = new CreateBatchOptions(); - /** - * Keeps track of the opened send links. Links are key'd by their entityPath. The send link for allowing the service - * load balance messages is the eventHubName. - */ - private final ConcurrentHashMap openLinks = new ConcurrentHashMap<>(); private final ClientLogger logger = new ClientLogger(EventHubProducerAsyncClient.class); private final AtomicBoolean isDisposed = new AtomicBoolean(); private final String fullyQualifiedNamespace; private final String eventHubName; - private final EventHubConnection connection; + private final EventHubConnectionProcessor connectionProcessor; private final AmqpRetryOptions retryOptions; private final TracerProvider tracerProvider; private final MessageSerializer messageSerializer; @@ -114,15 +110,17 @@ public class EventHubProducerAsyncClient implements Closeable { * when {@link CreateBatchOptions#getPartitionId()} is not null or an empty string. Otherwise, allows the service to * load balance the messages amongst available partitions. */ - EventHubProducerAsyncClient(String fullyQualifiedNamespace, String eventHubName, EventHubConnection connection, - AmqpRetryOptions retryOptions, TracerProvider tracerProvider, MessageSerializer messageSerializer, - boolean isSharedConnection) { - this.fullyQualifiedNamespace = fullyQualifiedNamespace; - this.eventHubName = eventHubName; - this.connection = connection; - this.retryOptions = retryOptions; - this.tracerProvider = tracerProvider; - this.messageSerializer = messageSerializer; + EventHubProducerAsyncClient(String fullyQualifiedNamespace, String eventHubName, + EventHubConnectionProcessor connectionProcessor, AmqpRetryOptions retryOptions, TracerProvider tracerProvider, + MessageSerializer messageSerializer, boolean isSharedConnection) { + this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, + "'fullyQualifiedNamespace' cannot be null."); + this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null."); + this.connectionProcessor = Objects.requireNonNull(connectionProcessor, + "'connectionProcessor' cannot be null."); + this.retryOptions = Objects.requireNonNull(retryOptions, "'retryOptions' cannot be null."); + this.tracerProvider = Objects.requireNonNull(tracerProvider, "'tracerProvider' cannot be null."); + this.messageSerializer = Objects.requireNonNull(messageSerializer, "'messageSerializer' cannot be null."); this.isSharedConnection = isSharedConnection; } @@ -152,7 +150,8 @@ public String getEventHubName() { */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono getEventHubProperties() { - return connection.getManagementNode().flatMap(EventHubManagementNode::getEventHubProperties); + return connectionProcessor.flatMap(connection -> connection.getManagementNode()) + .flatMap(EventHubManagementNode::getEventHubProperties); } /** @@ -169,14 +168,13 @@ public Flux getPartitionIds() { * 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. - * * @throws NullPointerException if {@code partitionId} is null. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono getPartitionProperties(String partitionId) { - return connection.getManagementNode().flatMap(node -> node.getPartitionProperties(partitionId)); + return connectionProcessor.flatMap(connection -> connection.getManagementNode()) + .flatMap(node -> node.getPartitionProperties(partitionId)); } /** @@ -192,9 +190,7 @@ public Mono createBatch() { * Creates an {@link EventDataBatch} configured with the options specified. * * @param options A set of options used to configure the {@link EventDataBatch}. - * * @return A new {@link EventDataBatch} that can fit as many events as the transport allows. - * * @throws NullPointerException if {@code options} is null. */ public Mono createBatch(CreateBatchOptions options) { @@ -253,7 +249,6 @@ public Mono createBatch(CreateBatchOptions options) { *

* * @param event Event to send to the service. - * * @return A {@link Mono} that completes when the event is pushed to the service. */ Mono send(EventData event) { @@ -276,7 +271,6 @@ Mono send(EventData event) { * * @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. */ Mono send(EventData event, SendOptions options) { @@ -295,7 +289,6 @@ Mono send(EventData event, SendOptions options) { * 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. */ Mono send(Iterable events) { @@ -313,7 +306,6 @@ Mono send(Iterable events) { * * @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. */ Mono send(Iterable events, SendOptions options) { @@ -332,7 +324,6 @@ Mono send(Iterable events, SendOptions options) { * 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. */ Mono send(Flux events) { @@ -350,7 +341,6 @@ Mono send(Flux events) { * * @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. */ Mono send(Flux events, SendOptions options) { @@ -367,9 +357,7 @@ Mono send(Flux events, SendOptions options) { * Sends the batch to the associated Event Hub. * * @param batch The batch to send to the service. - * * @return A {@link Mono} that completes when the batch is pushed to the service. - * * @throws NullPointerException if {@code batch} is {@code null}. * @see EventHubProducerAsyncClient#createBatch() * @see EventHubProducerAsyncClient#createBatch(CreateBatchOptions) @@ -490,14 +478,11 @@ private String getLinkName(String partitionId) { private Mono getSendLink(String partitionId) { final String entityPath = getEntityPath(partitionId); - final AmqpSendLink openLink = openLinks.get(entityPath); - if (openLink != null) { - return Mono.just(openLink); - } else { - return connection.createSendLink(getLinkName(partitionId), entityPath, retryOptions) - .map(link -> openLinks.computeIfAbsent(entityPath, unusedKey -> link)); - } + return connectionProcessor + .flatMap(connection -> { + return connection.createSendLink(getLinkName(partitionId), entityPath, retryOptions); + }); } /** @@ -510,11 +495,8 @@ public void close() { return; } - openLinks.forEach((key, value) -> value.close()); - openLinks.clear(); - if (!isSharedConnection) { - connection.close(); + connectionProcessor.dispose(); } } From 29b3a99806f563ea57232f419388d55734786f06 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:43:09 -0800 Subject: [PATCH 13/31] Update tests. --- .../EventHubConsumerAsyncClientTest.java | 43 +-- ...EventHubConsumerClientIntegrationTest.java | 3 +- .../eventhubs/EventHubConsumerClientTest.java | 13 +- .../EventHubProducerAsyncClientTest.java | 244 +++++++++++------- .../eventhubs/EventHubProducerClientTest.java | 19 +- .../EventHubConnectionProcessorTest.java | 10 +- 6 files changed, 196 insertions(+), 136 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java index 7aa341a6d98b..f16d3bcc0c59 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java @@ -14,6 +14,7 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.util.logging.ClientLogger; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; @@ -81,6 +82,7 @@ public class EventHubConsumerAsyncClientTest { private static final String PARTITION_ID = "a-partition-id"; private final ClientLogger logger = new ClientLogger(EventHubConsumerAsyncClientTest.class); + private final AmqpRetryOptions retryOptions = new AmqpRetryOptions().setMaxRetries(2); private final String messageTrackingUUID = UUID.randomUUID().toString(); private final DirectProcessor endpointProcessor = DirectProcessor.create(); private final DirectProcessor messageProcessor = DirectProcessor.create(); @@ -93,14 +95,14 @@ public class EventHubConsumerAsyncClientTest { private EventHubSession session; @Mock private TokenCredential tokenCredential; + @Mock + private EventHubConnectionProcessor connectionProcessor; @Captor private ArgumentCaptor> creditSupplier; - private EventHubConnection eventHubConnection; private MessageSerializer messageSerializer = new EventHubMessageSerializer(); private EventHubConsumerAsyncClient consumer; - private ConnectionOptions connectionOptions; @BeforeEach public void setup() { @@ -109,15 +111,14 @@ public void setup() { when(amqpReceiveLink.receive()).thenReturn(messageProcessor); when(amqpReceiveLink.getEndpointStates()).thenReturn(endpointProcessor); - connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, + ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - eventHubConnection = new EventHubConnection(Mono.just(connection), connectionOptions); when(connection.createSession(any())).thenReturn(Mono.just(session)); when(session.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) .thenReturn(Mono.just(amqpReceiveLink)); - consumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, eventHubConnection, messageSerializer, + consumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false); } @@ -128,13 +129,13 @@ public void teardown() { } /** - * Verify that by default, lastEnqueuedInformation is null if - * {@link ReceiveOptions#getTrackLastEnqueuedEventProperties()} is not set. + * Verify that by default, lastEnqueuedInformation is null if {@link ReceiveOptions#getTrackLastEnqueuedEventProperties()} + * is not set. */ @Test public void lastEnqueuedEventInformationIsNull() { final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - eventHubConnection, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); + connectionProcessor, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); final int numberOfEvents = 10; when(amqpReceiveLink.getCredits()).thenReturn(numberOfEvents); final int numberToReceive = 3; @@ -155,7 +156,7 @@ public void lastEnqueuedEventInformationIsNull() { public void lastEnqueuedEventInformationCreated() { // Arrange final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - eventHubConnection, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); + connectionProcessor, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); final int numberOfEvents = 10; final ReceiveOptions receiveOptions = new ReceiveOptions().setTrackLastEnqueuedEventProperties(true); when(amqpReceiveLink.getCredits()).thenReturn(numberOfEvents); @@ -203,8 +204,8 @@ public void returnsNewListener() { final int numberOfEvents = 10; EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnection eventHubConnection = new EventHubConnection(Mono.fromCallable(() -> connection1), - connectionOptions); + EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); EmitterProcessor processor2 = EmitterProcessor.create(); FluxSink processor2sink = processor2.sink(); @@ -505,8 +506,8 @@ public void receivesMultiplePartitions() { when(amqpReceiveLink.getCredits()).thenReturn(numberOfEvents); EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnection eventHubConnection = new EventHubConnection(Mono.fromCallable(() -> connection1), - connectionOptions); + EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); String id2 = "partition-2"; String id3 = "partition-3"; @@ -579,8 +580,8 @@ public void receivesMultiplePartitionsWhenOneCloses() { final FluxSink processor1sink = messageProcessor.sink(); EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnection eventHubConnection = new EventHubConnection(Mono.fromCallable(() -> connection1), - connectionOptions); + EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); String id2 = "partition-2"; String id3 = "partition-3"; @@ -656,15 +657,17 @@ public void receivesMultiplePartitionsWhenOneCloses() { @Test public void doesNotCloseSharedConnection() { // Arrange - EventHubConnection hubConnection = mock(EventHubConnection.class); + EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); + EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); EventHubConsumerAsyncClient sharedConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - hubConnection, messageSerializer, CONSUMER_GROUP, PREFETCH, true); + eventHubConnection, messageSerializer, CONSUMER_GROUP, PREFETCH, true); // Act sharedConsumer.close(); // Verify - verify(hubConnection, never()).close(); + verify(eventHubConnection, never()).dispose(); } /** @@ -673,7 +676,7 @@ public void doesNotCloseSharedConnection() { @Test public void closesDedicatedConnection() { // Arrange - EventHubConnection hubConnection = mock(EventHubConnection.class); + EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubConsumerAsyncClient dedicatedConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, hubConnection, messageSerializer, CONSUMER_GROUP, PREFETCH, false); @@ -681,7 +684,7 @@ public void closesDedicatedConnection() { dedicatedConsumer.close(); // Verify - verify(hubConnection, times(1)).close(); + verify(hubConnection, times(1)).dispose(); } private void assertPartition(String partitionId, PartitionEvent event) { diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientIntegrationTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientIntegrationTest.java index 1d1a40315df3..51d1050c89a7 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientIntegrationTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientIntegrationTest.java @@ -37,7 +37,6 @@ public class EventHubConsumerClientIntegrationTest extends IntegrationTestBase { private EventHubClient client; private EventHubConsumerClient consumer; - private EventHubConnection connection; private EventPosition startingPosition; // We use these values to keep track of the events we've pushed to the service and ensure the events we receive are @@ -71,7 +70,7 @@ protected void beforeTest() { @Override protected void afterTest() { - dispose(consumer, connection, client); + dispose(consumer, client); } /** diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java index 6940f7b8457c..950b66ed81a2 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java @@ -13,6 +13,7 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.util.IterableStream; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; import com.azure.messaging.eventhubs.models.LastEnqueuedEventProperties; @@ -84,7 +85,7 @@ public class EventHubConsumerClientTest { private TokenCredential tokenCredential; private EventHubConsumerClient consumer; - private EventHubConnection linkProvider; + private EventHubConnectionProcessor connectionProcessor; private ConnectionOptions connectionOptions; private EventHubConsumerAsyncClient asyncConsumer; @@ -100,7 +101,9 @@ public void setup() { connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - linkProvider = new EventHubConnection(Mono.just(connection), connectionOptions); + connectionProcessor = Mono.fromCallable(() -> connection).subscribeWith(new EventHubConnectionProcessor( + connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), + connectionOptions.getRetry())); when(connection.createSession(argThat(name -> name.endsWith(PARTITION_ID)))) .thenReturn(Mono.fromCallable(() -> session)); when(session.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) @@ -110,7 +113,7 @@ CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS })); asyncConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - linkProvider, messageSerializer, CONSUMER_GROUP, PREFETCH, false); + connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false); consumer = new EventHubConsumerClient(asyncConsumer, Duration.ofSeconds(10)); } @@ -133,7 +136,7 @@ public static void dispose() { public void lastEnqueuedEventInformationIsNull() { // Arrange final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient( - HOSTNAME, EVENT_HUB_NAME, linkProvider, messageSerializer, CONSUMER_GROUP, + HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false); final EventHubConsumerClient consumer = new EventHubConsumerClient(runtimeConsumer, Duration.ofSeconds(5)); final int numberOfEvents = 10; @@ -161,7 +164,7 @@ public void lastEnqueuedEventInformationCreated() { // Arrange final ReceiveOptions options = new ReceiveOptions().setTrackLastEnqueuedEventProperties(true); final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient( - HOSTNAME, EVENT_HUB_NAME, linkProvider, messageSerializer, CONSUMER_GROUP, PREFETCH, false); + HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false); final EventHubConsumerClient consumer = new EventHubConsumerClient(runtimeConsumer, Duration.ofSeconds(5)); final int numberOfEvents = 10; diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java index 198cb77c8407..0dfe5a44b642 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java @@ -3,11 +3,12 @@ package com.azure.messaging.eventhubs; +import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpRetryOptions; -import com.azure.core.amqp.AmqpSession; import com.azure.core.amqp.AmqpTransportType; import com.azure.core.amqp.ProxyOptions; import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpErrorContext; import com.azure.core.amqp.exception.AmqpException; import com.azure.core.amqp.implementation.AmqpSendLink; import com.azure.core.amqp.implementation.CbsAuthorizationType; @@ -20,6 +21,7 @@ import com.azure.core.util.tracing.Tracer; import com.azure.messaging.eventhubs.implementation.ClientConstants; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.models.CreateBatchOptions; import com.azure.messaging.eventhubs.models.SendOptions; import org.apache.qpid.proton.amqp.messaging.Section; @@ -33,7 +35,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -41,6 +45,7 @@ import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import static com.azure.core.util.tracing.Tracer.DIAGNOSTIC_ID_KEY; import static com.azure.core.util.tracing.Tracer.PARENT_SPAN_KEY; @@ -49,6 +54,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -66,37 +72,54 @@ public class EventHubProducerAsyncClientTest { @Mock private AmqpSendLink sendLink; @Mock - private AmqpSession session; + private AmqpSendLink sendLink2; + @Mock + private AmqpSendLink sendLink3; + @Mock private EventHubAmqpConnection connection; + @Mock + private EventHubAmqpConnection connection2; + @Mock + private EventHubAmqpConnection connection3; + @Mock + private TokenCredential tokenCredential; @Captor private ArgumentCaptor singleMessageCaptor; - @Captor private ArgumentCaptor> messagesCaptor; private final MessageSerializer messageSerializer = new EventHubMessageSerializer(); private final AmqpRetryOptions retryOptions = new AmqpRetryOptions().setTryTimeout(Duration.ofSeconds(10)); + private final DirectProcessor endpointProcessor = DirectProcessor.create(); + private final FluxSink endpointSink = endpointProcessor.sink(FluxSink.OverflowStrategy.BUFFER); private EventHubProducerAsyncClient producer; - private EventHubConnection eventHubConnection; - @Mock - private TokenCredential tokenCredential; + private EventHubConnectionProcessor connectionProcessor; private TracerProvider tracerProvider; + private ConnectionOptions connectionOptions; @BeforeEach public void setup() { MockitoAnnotations.initMocks(this); tracerProvider = new TracerProvider(Collections.emptyList()); - ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, + connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - eventHubConnection = new EventHubConnection(Mono.just(connection), connectionOptions); - producer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, eventHubConnection, retryOptions, tracerProvider, - messageSerializer, false); + + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + + connectionProcessor = Mono.fromCallable(() -> connection).repeat(10).subscribeWith( + new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); + producer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, retryOptions, + tracerProvider, messageSerializer, false); when(sendLink.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); + when(sendLink2.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); + when(sendLink3.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); } @AfterEach @@ -122,11 +145,8 @@ public void sendMultipleMessages() { }); final SendOptions options = new SendOptions(); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(sendLink)); when(sendLink.send(anyList())).thenReturn(Mono.empty()); @@ -152,12 +172,10 @@ public void sendSingleMessage() { final EventData testData = new EventData(TEST_CONTENTS.getBytes(UTF_8)); final SendOptions options = new SendOptions(); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(sendLink)); + when(sendLink.send(any(Message.class))).thenReturn(Mono.empty()); // Act @@ -182,11 +200,8 @@ public void partitionProducerCannotSendWithPartitionKey() { new EventData(TEST_CONTENTS.getBytes(UTF_8)), new EventData(TEST_CONTENTS.getBytes(UTF_8))); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(sendLink)); when(sendLink.send(anyList())).thenReturn(Mono.empty()); @@ -220,15 +235,12 @@ public void sendStartSpanSingleMessage() { final SendOptions sendOptions = new SendOptions() .setPartitionId(partitionId); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - eventHubConnection, retryOptions, tracerProvider, messageSerializer, false); - - when(connection.createSession(argThat(name -> name.endsWith(partitionId)))) - .thenReturn(Mono.just(session)); - when(session.createProducer( - argThat(name -> name.startsWith("PS")), - argThat(name -> name.endsWith(partitionId)), - eq(retryOptions.getTryTimeout()), any())) + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); + + when(connection.createSendLink( + argThat(name -> name.startsWith("PS")), argThat(name -> name.endsWith(partitionId)), eq(retryOptions))) .thenReturn(Mono.just(sendLink)); + when(sendLink.send(anyList())).thenReturn(Mono.empty()); when(tracer1.start(eq("EventHubs.send"), any(), eq(ProcessKind.SEND))).thenAnswer( @@ -265,7 +277,8 @@ public void sendStartSpanSingleMessage() { } /** - * Verifies addLink method is not invoked and message/event is not stamped with context on retry (span context already present on event). + * Verifies addLink method is not invoked and message/event is not stamped with context on retry (span context + * already present on event). */ @Test public void sendMessageRetrySpanTest() { @@ -276,7 +289,7 @@ public void sendMessageRetrySpanTest() { final EventData eventData = new EventData(TEST_CONTENTS.getBytes(UTF_8)) .addContext(SPAN_CONTEXT_KEY, Context.NONE); - final EventData eventData2 = new EventData(TEST_CONTENTS.getBytes(UTF_8)) + final EventData eventData2 = new EventData(TEST_CONTENTS.getBytes(UTF_8)) .addContext(SPAN_CONTEXT_KEY, Context.NONE); final Flux testData = Flux.just(eventData, eventData2); @@ -284,14 +297,10 @@ public void sendMessageRetrySpanTest() { final SendOptions sendOptions = new SendOptions() .setPartitionId(partitionId); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - eventHubConnection, retryOptions, tracerProvider, messageSerializer, false); - - when(connection.createSession(argThat(name -> name.endsWith(partitionId)))) - .thenReturn(Mono.just(session)); - when(session.createProducer( - argThat(name -> name.startsWith("PS")), - argThat(name -> name.endsWith(partitionId)), - eq(retryOptions.getTryTimeout()), any())) + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); + + when(connection.createSendLink( + argThat(name -> name.startsWith("PS")), argThat(name -> name.endsWith(partitionId)), eq(retryOptions))) .thenReturn(Mono.just(sendLink)); when(sendLink.send(anyList())).thenReturn(Mono.empty()); @@ -330,11 +339,8 @@ public void sendTooManyMessages() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // We believe 20 events is enough for that EventDataBatch to be greater than max size. @@ -366,11 +372,9 @@ public void createsEventDataBatch() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -398,8 +402,8 @@ public void createsEventDataBatch() { } /** - * Verifies that message spans are started and ended on tryAdd when creating batches to send in - * {@link EventDataBatch}. + * Verifies that message spans are started and ended on tryAdd when creating batches to send in {@link + * EventDataBatch}. */ @Test public void startMessageSpansOnCreateBatch() { @@ -408,15 +412,13 @@ public void startMessageSpansOnCreateBatch() { final List tracers = Collections.singletonList(tracer1); TracerProvider tracerProvider = new TracerProvider(tracers); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - eventHubConnection, retryOptions, tracerProvider, messageSerializer, false); + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); when(tracer1.start(eq("EventHubs.message"), any(), eq(ProcessKind.MESSAGE))).thenAnswer( @@ -451,11 +453,9 @@ public void createsEventDataBatchWithPartitionKey() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -482,11 +482,9 @@ public void createEventDataBatchWhenMaxSizeIsTooBig() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -514,11 +512,9 @@ public void createsEventDataBatchWithSize() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -591,11 +587,9 @@ public void batchOptionsIsCloned() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); final String originalKey = "some-key"; @@ -621,11 +615,9 @@ public void sendsAnEventDataBatch() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -668,37 +660,25 @@ public void sendMultiplePartitions() { final String partitionId1 = "my-partition-id"; final String partitionId2 = "my-partition-id-2"; - final AmqpSession partition1Session = mock(AmqpSession.class); - final AmqpSession partition2Session = mock(AmqpSession.class); - final AmqpSendLink sendLink1 = mock(AmqpSendLink.class); - final AmqpSendLink sendLink2 = mock(AmqpSendLink.class); + when(sendLink2.send(anyList())).thenReturn(Mono.empty()); + when(sendLink2.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); + when(sendLink3.send(anyList())).thenReturn(Mono.empty()); + when(sendLink3.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); - when(connection.createSession(any())).thenAnswer(mock -> { - final String entityPath = mock.getArgument(0, String.class); + // EC is the prefix they use when creating a link that sends to the service round-robin. + when(connection.createSendLink(anyString(), anyString(), any())).thenAnswer(mock -> { + final String entityPath = mock.getArgument(1, String.class); if (EVENT_HUB_NAME.equals(entityPath)) { - return Mono.just(session); + return Mono.just(sendLink); } else if (entityPath.endsWith(partitionId1)) { - return Mono.just(partition1Session); + return Mono.just(sendLink3); } else if (entityPath.endsWith(partitionId2)) { - return Mono.just(partition2Session); + return Mono.just(sendLink2); } else { return Mono.error(new IllegalArgumentException("Could not figure out entityPath: " + entityPath)); } }); - when(partition1Session.createProducer(any(), argThat(name -> name.endsWith(partitionId1)), any(), any())) - .thenReturn(Mono.just(sendLink1)); - when(partition2Session.createProducer(any(), argThat(name -> name.endsWith(partitionId2)), any(), any())) - .thenReturn(Mono.just(sendLink2)); - when(sendLink1.send(anyList())).thenReturn(Mono.empty()); - when(sendLink1.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); - when(sendLink2.send(anyList())).thenReturn(Mono.empty()); - when(sendLink2.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); - - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) - .thenReturn(Mono.just(sendLink)); when(sendLink.send(anyList())).thenReturn(Mono.empty()); // Act @@ -717,18 +697,17 @@ public void sendMultiplePartitions() { final List messagesSent = messagesCaptor.getValue(); Assertions.assertEquals(count, messagesSent.size()); - verify(sendLink1, times(1)).send(anyList()); + verify(sendLink3, times(1)).send(anyList()); verify(sendLink2, times(1)).send(anyList()); } - /** * Verifies that when we have a shared connection, the producer does not close that connection. */ @Test public void doesNotCloseSharedConnection() { // Arrange - EventHubConnection hubConnection = mock(EventHubConnection.class); + EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient sharedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, hubConnection, retryOptions, tracerProvider, messageSerializer, true); @@ -736,7 +715,7 @@ public void doesNotCloseSharedConnection() { sharedProducer.close(); // Verify - verify(hubConnection, never()).close(); + verify(hubConnection, never()).dispose(); } /** @@ -745,7 +724,7 @@ public void doesNotCloseSharedConnection() { @Test public void closesDedicatedConnection() { // Arrange - EventHubConnection hubConnection = mock(EventHubConnection.class); + EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient dedicatedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, hubConnection, retryOptions, tracerProvider, messageSerializer, false); @@ -753,17 +732,16 @@ public void closesDedicatedConnection() { dedicatedProducer.close(); // Verify - verify(hubConnection, times(1)).close(); + verify(hubConnection, times(1)).dispose(); } - /** * Verifies that when we have a non-shared connection, the producer closes that connection. Only once. */ @Test public void closesDedicatedConnectionOnlyOnce() { // Arrange - EventHubConnection hubConnection = mock(EventHubConnection.class); + EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient dedicatedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, hubConnection, retryOptions, tracerProvider, messageSerializer, false); @@ -772,9 +750,77 @@ public void closesDedicatedConnectionOnlyOnce() { dedicatedProducer.close(); // Verify - verify(hubConnection, times(1)).close(); + verify(hubConnection, times(1)).dispose(); } + @Test + public void reopensOnFailure() { + // Arrange + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + + EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ + connection, connection2, connection3 + }; + connectionProcessor = Flux.create(sink -> { + final AtomicInteger count = new AtomicInteger(); + sink.onRequest(request -> { + for (int i = 0; i < request; i++) { + final int current = count.getAndIncrement(); + final int index = current % connections.length; + sink.next(connections[index]); + } + }); + }).subscribeWith( + new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); + producer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, retryOptions, + tracerProvider, messageSerializer, false); + + final int count = 4; + final byte[] contents = TEST_CONTENTS.getBytes(UTF_8); + final Flux testData = Flux.range(0, count).flatMap(number -> { + final EventData data = new EventData(contents); + return Flux.just(data); + }); + final EventData testData2 = new EventData("test"); + + // EC is the prefix they use when creating a link that sends to the service round-robin. + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) + .thenReturn(Mono.just(sendLink)); + when(sendLink.send(anyList())).thenReturn(Mono.empty()); + + final DirectProcessor connectionState2 = DirectProcessor.create(); + when(connection2.getEndpointStates()).thenReturn(connectionState2); + when(connection2.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) + .thenReturn(Mono.just(sendLink2)); + when(sendLink2.send(any(Message.class))).thenReturn(Mono.empty()); + + final DirectProcessor connectionState3 = DirectProcessor.create(); + when(connection3.getEndpointStates()).thenReturn(connectionState3); + when(connection3.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) + .thenReturn(Mono.just(sendLink3)); + when(sendLink3.send(anyList())).thenReturn(Mono.empty()); + + // Act + StepVerifier.create(producer.send(testData)) + .verifyComplete(); + + // Send in an error signal like a server busy condition. + endpointSink.error(new AmqpException(true, AmqpErrorCondition.SERVER_BUSY_ERROR, "Test-message", + new AmqpErrorContext("test-namespace"))); + + StepVerifier.create(producer.send(testData2)) + .verifyComplete(); + + // Assert + verify(sendLink).send(messagesCaptor.capture()); + final List messagesSent = messagesCaptor.getValue(); + Assertions.assertEquals(count, messagesSent.size()); + + verify(sendLink2, times(1)).send(any(Message.class)); + verifyZeroInteractions(sendLink3); + } static final String TEST_CONTENTS = "SSLorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula posuere lobortis. Aliquam finibus volutpat dolor, faucibus pellentesque ipsum bibendum vitae. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut sit amet urna hendrerit, dapibus justo a, sodales justo. Mauris finibus augue id pulvinar congue. Nam maximus luctus ipsum, at commodo ligula euismod ac. Phasellus vitae lacus sit amet diam porta placerat. \n" + "Ut sodales efficitur sapien ut posuere. Morbi sed tellus est. Proin eu erat purus. Proin massa nunc, condimentum id iaculis dignissim, consectetur et odio. Cras suscipit sem eu libero aliquam tincidunt. Nullam ut arcu suscipit, eleifend velit in, cursus libero. Ut eleifend facilisis odio sit amet feugiat. Phasellus at nunc sit amet elit sagittis commodo ac in nisi. Fusce vitae aliquam quam. Integer vel nibh euismod, tempus elit vitae, pharetra est. Duis vulputate enim a elementum dignissim. Morbi dictum enim id elit scelerisque, in elementum nulla pharetra. \n" diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java index 246753b9273c..be5f1d2611d3 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java @@ -21,6 +21,7 @@ import com.azure.core.util.tracing.Tracer; import com.azure.messaging.eventhubs.implementation.ClientConstants; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; +import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.models.CreateBatchOptions; import com.azure.messaging.eventhubs.models.SendOptions; import org.apache.qpid.proton.amqp.messaging.Section; @@ -71,6 +72,8 @@ public class EventHubProducerClientTest { private AmqpSession session; @Mock private EventHubAmqpConnection connection; + @Mock + private TokenCredential tokenCredential; @Captor private ArgumentCaptor singleMessageCaptor; @Captor @@ -79,9 +82,7 @@ public class EventHubProducerClientTest { private EventHubProducerAsyncClient asyncProducer; private AmqpRetryOptions retryOptions = new AmqpRetryOptions().setTryTimeout(Duration.ofSeconds(30)); private MessageSerializer messageSerializer = new EventHubMessageSerializer(); - private EventHubConnection linkProvider; - @Mock - private TokenCredential tokenCredential; + private EventHubConnectionProcessor connectionProcessor; @BeforeEach public void setup() { @@ -96,8 +97,10 @@ public void setup() { ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - linkProvider = new EventHubConnection(Mono.just(connection), connectionOptions); - asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, linkProvider, retryOptions, + connectionProcessor = Mono.fromCallable(() -> connection).subscribeWith(new EventHubConnectionProcessor( + connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), + connectionOptions.getRetry())); + asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); } @@ -146,7 +149,7 @@ public void sendStartSpanSingleMessage() { final List tracers = Collections.singletonList(tracer1); final TracerProvider tracerProvider = new TracerProvider(tracers); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - linkProvider, retryOptions, tracerProvider, messageSerializer, false); + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); final EventData eventData = new EventData("hello-world".getBytes(UTF_8)); @@ -205,7 +208,7 @@ public void sendMessageRetrySpanTest() { .thenReturn(Mono.just(sendLink)); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - linkProvider, retryOptions, tracerProvider, messageSerializer, false); + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); final EventData eventData = new EventData("hello-world".getBytes(UTF_8)) .addContext(SPAN_CONTEXT_KEY, Context.NONE); @@ -320,7 +323,7 @@ public void startsMessageSpanOnEventBatch() { final List tracers = Collections.singletonList(tracer1); final TracerProvider tracerProvider = new TracerProvider(tracers); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, - linkProvider, retryOptions, tracerProvider, messageSerializer, false); + connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); final AmqpSendLink link = mock(AmqpSendLink.class); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index d2c3c7e2103e..076cac1a150c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -17,6 +17,7 @@ package com.azure.messaging.eventhubs.implementation; import com.azure.core.amqp.AmqpEndpointState; +import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpShutdownSignal; import com.azure.core.amqp.exception.AmqpErrorCondition; import com.azure.core.amqp.exception.AmqpErrorContext; @@ -46,6 +47,11 @@ * Tests for {@link EventHubConnectionProcessor}. */ public class EventHubConnectionProcessorTest { + private static final String NAMESPACE = "test-namespace.eventhubs.com"; + private static final String EVENT_HUB_NAME = "test-event-hub-name"; + private static final AmqpRetryOptions AMQP_RETRY_OPTIONS = new AmqpRetryOptions().setMaxRetries(2) + .setDelay(Duration.ofSeconds(10)); + @Mock private EventHubAmqpConnection connection; @Mock @@ -56,8 +62,8 @@ public class EventHubConnectionProcessorTest { private final Duration timeout = Duration.ofSeconds(10); private DirectProcessor endpointProcessor = DirectProcessor.create(); private DirectProcessor shutdownSignalProcessor = DirectProcessor.create(); - - private EventHubConnectionProcessor eventHubConnectionProcessor = new EventHubConnectionProcessor(); + private EventHubConnectionProcessor eventHubConnectionProcessor = new EventHubConnectionProcessor(NAMESPACE, + EVENT_HUB_NAME, AMQP_RETRY_OPTIONS); @BeforeEach public void setup() { From 89f5e19a04085943bc310d91f6f5e055800ad965 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 00:53:18 -0800 Subject: [PATCH 14/31] Fix tests for consumers. --- .../EventHubConsumerAsyncClientTest.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java index f16d3bcc0c59..76a9a4865702 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java @@ -58,6 +58,7 @@ import static com.azure.messaging.eventhubs.TestUtils.isMatchingEvent; import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -85,6 +86,7 @@ public class EventHubConsumerAsyncClientTest { private final AmqpRetryOptions retryOptions = new AmqpRetryOptions().setMaxRetries(2); private final String messageTrackingUUID = UUID.randomUUID().toString(); private final DirectProcessor endpointProcessor = DirectProcessor.create(); + private final FluxSink endpointSink = endpointProcessor.sink(FluxSink.OverflowStrategy.BUFFER); private final DirectProcessor messageProcessor = DirectProcessor.create(); @Mock @@ -95,14 +97,13 @@ public class EventHubConsumerAsyncClientTest { private EventHubSession session; @Mock private TokenCredential tokenCredential; - @Mock - private EventHubConnectionProcessor connectionProcessor; @Captor private ArgumentCaptor> creditSupplier; private MessageSerializer messageSerializer = new EventHubMessageSerializer(); private EventHubConsumerAsyncClient consumer; + private EventHubConnectionProcessor connectionProcessor; @BeforeEach public void setup() { @@ -114,9 +115,15 @@ public void setup() { ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - when(connection.createSession(any())).thenReturn(Mono.just(session)); - when(session.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) - .thenReturn(Mono.just(amqpReceiveLink)); + + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + + when(connection.createReceiveLink(anyString(), argThat(name -> name.endsWith(PARTITION_ID)), + any(EventPosition.class), any(ReceiveOptions.class))).thenReturn(Mono.just(amqpReceiveLink)); + connectionProcessor = Flux.create(sink -> sink.next(connection)) + .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); consumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false); From abc2a70979f036f896c83344ebe0ebf6578fcaf4 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 11:19:54 -0800 Subject: [PATCH 15/31] Fixing EHubClientBuilder to repeat the mono, so we have a potentially infinite stream of connections. --- .../eventhubs/EventHubClientBuilder.java | 18 ++++++++++-------- .../EventHubConnectionProcessor.java | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java index fa88c34f2b96..41d6cb298aaa 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java @@ -29,6 +29,7 @@ import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubReactorAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubSharedKeyCredential; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -517,17 +518,18 @@ private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer m final ReactorProvider provider = new ReactorProvider(); final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider); - Map properties = CoreUtils.getProperties(EVENTHUBS_PROPERTIES_FILE); - String product = properties.getOrDefault(NAME_KEY, UNKNOWN); - String clientVersion = properties.getOrDefault(VERSION_KEY, UNKNOWN); + final Map properties = CoreUtils.getProperties(EVENTHUBS_PROPERTIES_FILE); + final String product = properties.getOrDefault(NAME_KEY, UNKNOWN); + final String clientVersion = properties.getOrDefault(VERSION_KEY, UNKNOWN); - final Mono connectionMono = Mono.fromCallable(() -> { + final Flux connectionFlux = Mono.fromCallable(() -> { final String connectionId = StringUtil.getRandomString("MF"); - return new EventHubReactorAmqpConnection(connectionId, connectionOptions, provider, handlerProvider, - tokenManagerProvider, messageSerializer, product, clientVersion); - }); - return connectionMono.subscribeWith(new EventHubConnectionProcessor( + return (EventHubAmqpConnection) new EventHubReactorAmqpConnection(connectionId, connectionOptions, provider, + handlerProvider, tokenManagerProvider, messageSerializer, product, clientVersion); + }).repeat(); + + return connectionFlux.subscribeWith(new EventHubConnectionProcessor( connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), connectionOptions.getRetry())); } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 9f477651b11a..f569ec2a55e8 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -85,6 +85,8 @@ public AmqpRetryOptions getRetryOptions() { @Override public void onSubscribe(Subscription subscription) { + logger.verbose("Subscribing to upstream for connections."); + this.upstream = subscription; // Don't request an EventHubAmqpConnection until there is a subscriber. @@ -160,7 +162,7 @@ public void onError(Throwable throwable) { @Override public void onComplete() { - logger.info("Completing EventHubConnectionSubscriber."); + logger.info("Upstream connection publisher was completed. Terminating EventHubConnectionProcessor."); isTerminated.set(true); synchronized (lock) { @@ -173,7 +175,7 @@ public void onComplete() { @Override public void subscribe(CoreSubscriber actual) { - logger.info("Subscription received."); + logger.verbose("Subscription received."); final ConnectionSubscriber subscriber = new ConnectionSubscriber(actual, this); actual.onSubscribe(subscriber); From aec04f6a3890bc25558f250bdac889e3baefd3e8 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 11:30:45 -0800 Subject: [PATCH 16/31] Add test to verify that on non-transient errors, the connection is not revived. --- .../EventHubConnectionProcessor.java | 2 +- .../EventHubProducerAsyncClientTest.java | 131 ++++++++++++++---- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index f569ec2a55e8..948d2023089c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -147,7 +147,7 @@ public void onError(Throwable throwable) { return; } - logger.warning("Non-retryable error occurred in connection. Error: {}", throwable); + logger.warning("Non-retryable error occurred in connection.", throwable); lastError = throwable; isTerminated.set(true); dispose(); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java index 0dfe5a44b642..95342d200e2f 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java @@ -65,7 +65,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; -public class EventHubProducerAsyncClientTest { +class EventHubProducerAsyncClientTest { private static final String HOSTNAME = "my-host-name"; private static final String EVENT_HUB_NAME = "my-event-hub-name"; @@ -100,7 +100,7 @@ public class EventHubProducerAsyncClientTest { private ConnectionOptions connectionOptions; @BeforeEach - public void setup() { + void setup() { MockitoAnnotations.initMocks(this); tracerProvider = new TracerProvider(Collections.emptyList()); @@ -123,7 +123,7 @@ public void setup() { } @AfterEach - public void teardown() { + void teardown() { Mockito.framework().clearInlineMocks(); sendLink = null; connection = null; @@ -135,7 +135,7 @@ public void teardown() { * Verifies that sending multiple events will result in calling producer.send(List<Message>). */ @Test - public void sendMultipleMessages() { + void sendMultipleMessages() { // Arrange final int count = 4; final byte[] contents = TEST_CONTENTS.getBytes(UTF_8); @@ -167,7 +167,7 @@ public void sendMultipleMessages() { * Verifies that sending a single event data will result in calling producer.send(Message). */ @Test - public void sendSingleMessage() { + void sendSingleMessage() { // Arrange final EventData testData = new EventData(TEST_CONTENTS.getBytes(UTF_8)); final SendOptions options = new SendOptions(); @@ -194,7 +194,7 @@ public void sendSingleMessage() { * Verifies that a partitioned producer cannot also send events with a partition key. */ @Test - public void partitionProducerCannotSendWithPartitionKey() { + void partitionProducerCannotSendWithPartitionKey() { // Arrange final Flux testData = Flux.just( new EventData(TEST_CONTENTS.getBytes(UTF_8)), @@ -222,7 +222,7 @@ public void partitionProducerCannotSendWithPartitionKey() { * Verifies start and end span invoked when sending a single message. */ @Test - public void sendStartSpanSingleMessage() { + void sendStartSpanSingleMessage() { // Arrange final Tracer tracer1 = mock(Tracer.class); final List tracers = Collections.singletonList(tracer1); @@ -281,7 +281,7 @@ public void sendStartSpanSingleMessage() { * already present on event). */ @Test - public void sendMessageRetrySpanTest() { + void sendMessageRetrySpanTest() { //Arrange final Tracer tracer1 = mock(Tracer.class); final List tracers = Collections.singletonList(tracer1); @@ -333,7 +333,7 @@ public void sendMessageRetrySpanTest() { * Verifies that it fails if we try to send multiple messages that cannot fit in a single message batch. */ @Test - public void sendTooManyMessages() { + void sendTooManyMessages() { // Arrange int maxLinkSize = 1024; final AmqpSendLink link = mock(AmqpSendLink.class); @@ -362,7 +362,7 @@ public void sendTooManyMessages() { * link. */ @Test - public void createsEventDataBatch() { + void createsEventDataBatch() { // Arrange int maxLinkSize = 1024; @@ -406,7 +406,7 @@ public void createsEventDataBatch() { * EventDataBatch}. */ @Test - public void startMessageSpansOnCreateBatch() { + void startMessageSpansOnCreateBatch() { // Arrange final Tracer tracer1 = mock(Tracer.class); final List tracers = Collections.singletonList(tracer1); @@ -444,7 +444,7 @@ public void startMessageSpansOnCreateBatch() { * Verifies we can create an EventDataBatch with partition key and link size. */ @Test - public void createsEventDataBatchWithPartitionKey() { + void createsEventDataBatchWithPartitionKey() { // Arrange int maxLinkSize = 1024; @@ -475,7 +475,7 @@ public void createsEventDataBatchWithPartitionKey() { * Verifies we cannot create an EventDataBatch if the BatchOptions size is larger than the link. */ @Test - public void createEventDataBatchWhenMaxSizeIsTooBig() { + void createEventDataBatchWhenMaxSizeIsTooBig() { // Arrange int maxLinkSize = 1024; int batchSize = maxLinkSize + 10; @@ -501,7 +501,7 @@ public void createEventDataBatchWhenMaxSizeIsTooBig() { * CreateBatchOptions#getMaximumSizeInBytes()}. */ @Test - public void createsEventDataBatchWithSize() { + void createsEventDataBatchWithSize() { // Arrange int maxLinkSize = 10000; int batchSize = 1024; @@ -542,7 +542,7 @@ public void createsEventDataBatchWithSize() { } @Test - public void sendEventRequired() { + void sendEventRequired() { // Arrange final EventData event = new EventData("Event-data"); final SendOptions sendOptions = new SendOptions(); @@ -555,7 +555,7 @@ public void sendEventRequired() { } @Test - public void sendEventIterableRequired() { + void sendEventIterableRequired() { // Arrange final List event = Collections.singletonList(new EventData("Event-data")); final SendOptions sendOptions = new SendOptions(); @@ -568,7 +568,7 @@ public void sendEventIterableRequired() { } @Test - public void sendEventFluxRequired() { + void sendEventFluxRequired() { // Arrange final Flux event = Flux.just(new EventData("Event-data")); final SendOptions sendOptions = new SendOptions(); @@ -581,7 +581,7 @@ public void sendEventFluxRequired() { } @Test - public void batchOptionsIsCloned() { + void batchOptionsIsCloned() { // Arrange int maxLinkSize = 1024; @@ -605,7 +605,7 @@ public void batchOptionsIsCloned() { } @Test - public void sendsAnEventDataBatch() { + void sendsAnEventDataBatch() { // Arrange int maxLinkSize = 1024; @@ -648,7 +648,7 @@ public void sendsAnEventDataBatch() { * Verify we can send messages to multiple partitionIds with same sender. */ @Test - public void sendMultiplePartitions() { + void sendMultiplePartitions() { // Arrange final int count = 4; final byte[] contents = TEST_CONTENTS.getBytes(UTF_8); @@ -705,7 +705,7 @@ public void sendMultiplePartitions() { * Verifies that when we have a shared connection, the producer does not close that connection. */ @Test - public void doesNotCloseSharedConnection() { + void doesNotCloseSharedConnection() { // Arrange EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient sharedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, @@ -722,7 +722,7 @@ public void doesNotCloseSharedConnection() { * Verifies that when we have a non-shared connection, the producer closes that connection. */ @Test - public void closesDedicatedConnection() { + void closesDedicatedConnection() { // Arrange EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient dedicatedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, @@ -739,7 +739,7 @@ public void closesDedicatedConnection() { * Verifies that when we have a non-shared connection, the producer closes that connection. Only once. */ @Test - public void closesDedicatedConnectionOnlyOnce() { + void closesDedicatedConnectionOnlyOnce() { // Arrange EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubProducerAsyncClient dedicatedProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, @@ -753,8 +753,11 @@ public void closesDedicatedConnectionOnlyOnce() { verify(hubConnection, times(1)).dispose(); } + /** + * Verifies that another link is received and we can continue publishing events on a transient failure. + */ @Test - public void reopensOnFailure() { + void reopensOnFailure() { // Arrange when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); @@ -822,7 +825,85 @@ public void reopensOnFailure() { verifyZeroInteractions(sendLink3); } - static final String TEST_CONTENTS = "SSLorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula posuere lobortis. Aliquam finibus volutpat dolor, faucibus pellentesque ipsum bibendum vitae. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut sit amet urna hendrerit, dapibus justo a, sodales justo. Mauris finibus augue id pulvinar congue. Nam maximus luctus ipsum, at commodo ligula euismod ac. Phasellus vitae lacus sit amet diam porta placerat. \n" + /** + * Verifies that on a non-transient failure, no more event hub connections are recreated and we can not send events. + * An error should be propagated back to us. + */ + @Test + void closesOnNonTransientFailure() { + // Arrange + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + + EventHubAmqpConnection[] connections = new EventHubAmqpConnection[]{ + connection, connection2, connection3 + }; + connectionProcessor = Flux.create(sink -> { + final AtomicInteger count = new AtomicInteger(); + sink.onRequest(request -> { + for (int i = 0; i < request; i++) { + final int current = count.getAndIncrement(); + final int index = current % connections.length; + sink.next(connections[index]); + } + }); + }).subscribeWith( + new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); + producer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, retryOptions, + tracerProvider, messageSerializer, false); + + final int count = 4; + final byte[] contents = TEST_CONTENTS.getBytes(UTF_8); + final Flux testData = Flux.range(0, count).flatMap(number -> { + final EventData data = new EventData(contents); + return Flux.just(data); + }); + final EventData testData2 = new EventData("test"); + + // EC is the prefix they use when creating a link that sends to the service round-robin. + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) + .thenReturn(Mono.just(sendLink)); + when(sendLink.send(anyList())).thenReturn(Mono.empty()); + + final DirectProcessor connectionState2 = DirectProcessor.create(); + when(connection2.getEndpointStates()).thenReturn(connectionState2); + when(connection2.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), eq(retryOptions))) + .thenReturn(Mono.just(sendLink2)); + when(sendLink2.send(any(Message.class))).thenReturn(Mono.empty()); + + final AmqpException nonTransientError = new AmqpException(false, AmqpErrorCondition.UNAUTHORIZED_ACCESS, + "Test unauthorized access", new AmqpErrorContext("test-namespace")); + + // Act + StepVerifier.create(producer.send(testData)) + .verifyComplete(); + + // Send in an error signal like authorization failure. + endpointSink.error(nonTransientError); + + StepVerifier.create(producer.send(testData2)) + .expectErrorSatisfies(error -> { + Assertions.assertTrue(error instanceof AmqpException); + + final AmqpException actual = (AmqpException) error; + Assertions.assertEquals(nonTransientError.isTransient(), actual.isTransient()); + Assertions.assertEquals(nonTransientError.getContext(), actual.getContext()); + Assertions.assertEquals(nonTransientError.getErrorCondition(), actual.getErrorCondition()); + Assertions.assertEquals(nonTransientError.getMessage(), actual.getMessage()); + }) + .verify(Duration.ofSeconds(10)); + + // Assert + verify(sendLink).send(messagesCaptor.capture()); + final List messagesSent = messagesCaptor.getValue(); + Assertions.assertEquals(count, messagesSent.size()); + + verifyZeroInteractions(sendLink2); + verifyZeroInteractions(sendLink3); + } + + private static final String TEST_CONTENTS = "SSLorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula posuere lobortis. Aliquam finibus volutpat dolor, faucibus pellentesque ipsum bibendum vitae. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut sit amet urna hendrerit, dapibus justo a, sodales justo. Mauris finibus augue id pulvinar congue. Nam maximus luctus ipsum, at commodo ligula euismod ac. Phasellus vitae lacus sit amet diam porta placerat. \n" + "Ut sodales efficitur sapien ut posuere. Morbi sed tellus est. Proin eu erat purus. Proin massa nunc, condimentum id iaculis dignissim, consectetur et odio. Cras suscipit sem eu libero aliquam tincidunt. Nullam ut arcu suscipit, eleifend velit in, cursus libero. Ut eleifend facilisis odio sit amet feugiat. Phasellus at nunc sit amet elit sagittis commodo ac in nisi. Fusce vitae aliquam quam. Integer vel nibh euismod, tempus elit vitae, pharetra est. Duis vulputate enim a elementum dignissim. Morbi dictum enim id elit scelerisque, in elementum nulla pharetra. \n" + "Aenean aliquet aliquet condimentum. Proin dapibus dui id libero tempus feugiat. Sed commodo ligula a lectus mattis, vitae tincidunt velit auctor. Fusce quis semper dui. Phasellus eu efficitur sem. Ut non sem sit amet enim condimentum venenatis id dictum massa. Nullam sagittis lacus a neque sodales, et ultrices arcu mattis. Aliquam erat volutpat. \n" + "Aenean fringilla quam elit, id mattis purus vestibulum nec. Praesent porta eros in dapibus molestie. Vestibulum orci libero, tincidunt et turpis eget, condimentum lobortis enim. Fusce suscipit ante et mauris consequat cursus nec laoreet lorem. Maecenas in sollicitudin diam, non tincidunt purus. Nunc mauris purus, laoreet eget interdum vitae, placerat a sapien. In mi risus, blandit eu facilisis nec, molestie suscipit leo. Pellentesque molestie urna vitae dui faucibus bibendum. \n" From 7a173c0f3cb61273fe048ffc68f3fce6135e1923 Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 11:32:38 -0800 Subject: [PATCH 17/31] Adding log messages. --- .../eventhubs/implementation/EventHubConnectionProcessor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 948d2023089c..3d21d47f4088 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -162,7 +162,7 @@ public void onError(Throwable throwable) { @Override public void onComplete() { - logger.info("Upstream connection publisher was completed. Terminating EventHubConnectionProcessor."); + logger.info("Upstream connection publisher was completed. Terminating processor."); isTerminated.set(true); synchronized (lock) { @@ -182,8 +182,10 @@ public void subscribe(CoreSubscriber actual) { if (isTerminated.get()) { if (lastError != null) { + logger.info("Processor is already terminated. Propagating error."); subscriber.onError(lastError); } else { + logger.info("Processor is already terminated. Propagating complete."); subscriber.onComplete(); } From d53234e3ee8e2332705698a69a8c9473bfa500ef Mon Sep 17 00:00:00 2001 From: Connie Date: Fri, 3 Jan 2020 12:55:19 -0800 Subject: [PATCH 18/31] Fix header. --- .../EventHubConnectionProcessor.java | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 3d21d47f4088..0ff38a16c82c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -1,18 +1,5 @@ -/* - * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.messaging.eventhubs.implementation; From 30d4ec327aac9fd67787789770181bea40e34521 Mon Sep 17 00:00:00 2001 From: Connie Date: Sat, 4 Jan 2020 06:59:41 -0800 Subject: [PATCH 19/31] Fix checkstyle. --- .../EventHubReactorAmqpConnection.java | 2 +- .../EventHubConnectionProcessorTest.java | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java index 178db62ee1a5..4c0bf642661f 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubReactorAmqpConnection.java @@ -141,7 +141,7 @@ public Mono createReceiveLink(String linkName, String entityPat @Override public void close() { logger.info("Disposing of connection."); - sendLinks.forEach((key, value) -> { value.close(); }); + sendLinks.forEach((key, value) -> value.close()); sendLinks.clear(); super.close(); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index 076cac1a150c..149314522cc5 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -1,18 +1,5 @@ -/* - * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.messaging.eventhubs.implementation; From ad9be4abf6fa659922302c461d9981885c3aec2d Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 6 Jan 2020 11:51:04 -0800 Subject: [PATCH 20/31] Remove logging for info. --- .../eventhubs/implementation/EventHubConnectionProcessor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 0ff38a16c82c..63bbd1731350 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -181,7 +181,6 @@ public void subscribe(CoreSubscriber actual) { synchronized (lock) { if (currentConnection != null) { - logger.info("Existing connection exists. Exposing that one."); subscriber.complete(currentConnection); return; } From 2a8dd64aadca183232c1ab1814ca5d682d86c35d Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 6 Jan 2020 17:40:20 -0800 Subject: [PATCH 21/31] Fix Event Hub Consumer tests. --- .../EventHubConsumerAsyncClientTest.java | 145 +++++++++--------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java index 76a9a4865702..e9e829e85b4b 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java @@ -70,7 +70,7 @@ /** * Unit tests to verify functionality of {@link EventHubConsumerAsyncClient}. */ -public class EventHubConsumerAsyncClientTest { +class EventHubConsumerAsyncClientTest { static final String PARTITION_ID_HEADER = "partition-id-sent"; private static final Duration TIMEOUT = Duration.ofSeconds(30); @@ -106,7 +106,7 @@ public class EventHubConsumerAsyncClientTest { private EventHubConnectionProcessor connectionProcessor; @BeforeEach - public void setup() { + void setup() { MockitoAnnotations.initMocks(this); when(amqpReceiveLink.receive()).thenReturn(messageProcessor); @@ -130,7 +130,7 @@ CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS } @AfterEach - public void teardown() { + void teardown() { Mockito.framework().clearInlineMocks(); consumer.close(); } @@ -140,7 +140,7 @@ public void teardown() { * is not set. */ @Test - public void lastEnqueuedEventInformationIsNull() { + void lastEnqueuedEventInformationIsNull() { final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); final int numberOfEvents = 10; @@ -160,7 +160,7 @@ public void lastEnqueuedEventInformationIsNull() { * Verify that the default information is set and is null because no information has been received. */ @Test - public void lastEnqueuedEventInformationCreated() { + void lastEnqueuedEventInformationCreated() { // Arrange final EventHubConsumerAsyncClient runtimeConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, DEFAULT_PREFETCH_COUNT, false); @@ -188,7 +188,7 @@ public void lastEnqueuedEventInformationCreated() { * prefetch value. */ @Test - public void receivesNumberOfEvents() { + void receivesNumberOfEvents() { // Arrange final int numberOfEvents = 10; @@ -206,23 +206,24 @@ public void receivesNumberOfEvents() { */ @SuppressWarnings("unchecked") @Test - public void returnsNewListener() { + void returnsNewListener() { // Arrange final int numberOfEvents = 10; EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + EventHubConnectionProcessor eventHubConnection = Flux.create(sink -> sink.next(connection1)) .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); + when(connection1.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + EmitterProcessor processor2 = EmitterProcessor.create(); FluxSink processor2sink = processor2.sink(); AmqpReceiveLink link2 = mock(AmqpReceiveLink.class); - EventHubSession session2 = mock(EventHubSession.class); EmitterProcessor processor3 = EmitterProcessor.create(); FluxSink processor3sink = processor3.sink(); AmqpReceiveLink link3 = mock(AmqpReceiveLink.class); - EventHubSession session3 = mock(EventHubSession.class); when(link2.receive()).thenReturn(processor2); when(link2.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); @@ -232,11 +233,8 @@ public void returnsNewListener() { when(link3.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); when(link3.getCredits()).thenReturn(numberOfEvents); - when(connection1.createSession(any())).thenReturn(Mono.just(session2), Mono.just(session3)); - when(session2.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) - .thenReturn(Mono.just(link2)); - when(session3.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) - .thenReturn(Mono.just(link3)); + when(connection1.createReceiveLink(any(), argThat(arg -> arg.endsWith(PARTITION_ID)), any(EventPosition.class), + any(ReceiveOptions.class))).thenReturn(Mono.just(link2), Mono.just(link3)); EventHubConsumerAsyncClient asyncClient = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, eventHubConnection, messageSerializer, CONSUMER_GROUP, PREFETCH, false); @@ -262,7 +260,7 @@ public void returnsNewListener() { * Verify that receive can have multiple subscribers. */ @Test - public void canHaveMultipleSubscribers() throws InterruptedException { + void canHaveMultipleSubscribers() throws InterruptedException { // Arrange final int numberOfEvents = 7; final CountDownLatch firstConsumerCountDown = new CountDownLatch(numberOfEvents); @@ -300,7 +298,7 @@ public void canHaveMultipleSubscribers() throws InterruptedException { * Verifies that we can limit the number of deliveries added on the link at a given time. */ @Test - public void canLimitRequestsBackpressure() throws InterruptedException { + void canLimitRequestsBackpressure() throws InterruptedException { // Arrange final int numberOfEvents = 20; final int backpressureRequest = 2; @@ -342,7 +340,7 @@ protected void hookOnNext(PartitionEvent value) { * Verifies that if we have limited the request, the number of credits added is the same as that limit. */ @Test - public void returnsCorrectCreditRequest() throws InterruptedException { + void returnsCorrectCreditRequest() throws InterruptedException { // Arrange final int numberOfEvents = 20; final int backpressureRequest = 2; @@ -384,7 +382,7 @@ protected void hookOnNext(PartitionEvent value) { * Verify that the correct number of credits are returned when the link is empty, and there are subscribers. */ @Test - public void suppliesCreditsWhenSubscribers() { + void suppliesCreditsWhenSubscribers() { // Arrange final int backPressure = 8; @@ -415,7 +413,7 @@ public void suppliesCreditsWhenSubscribers() { * Verify that 0 credits are returned when there are no subscribers for this link anymore. */ @Test - public void suppliesNoCreditsWhenNoSubscribers() { + void suppliesNoCreditsWhenNoSubscribers() { // Arrange final int backPressure = 8; @@ -445,7 +443,7 @@ public void suppliesNoCreditsWhenNoSubscribers() { * Verifies that the consumer closes and completes any listeners on a shutdown signal. */ @Test - public void listensToShutdownSignals() throws InterruptedException { + void listensToShutdownSignals() throws InterruptedException { // Arrange final int numberOfEvents = 7; final CountDownLatch shutdownReceived = new CountDownLatch(3); @@ -495,7 +493,7 @@ public void listensToShutdownSignals() throws InterruptedException { } @Test - public void setsCorrectProperties() { + void setsCorrectProperties() { // Act EventHubConsumerAsyncClient consumer = new EventHubClientBuilder() .connectionString("Endpoint=sb://doesnotexist.servicebus.windows.net/;SharedAccessKeyName=doesnotexist;SharedAccessKey=dGhpcyBpcyBub3QgYSB2YWxpZCBrZXkgLi4uLi4uLi4=;EntityPath=dummy-event-hub") @@ -508,17 +506,23 @@ public void setsCorrectProperties() { } @Test - public void receivesMultiplePartitions() { + void receivesMultiplePartitions() { + // Arrange int numberOfEvents = 10; when(amqpReceiveLink.getCredits()).thenReturn(numberOfEvents); EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + EventHubConnectionProcessor eventHubConnection = Flux.create(sink -> sink.next(connection1)) .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); + when(connection1.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + String id2 = "partition-2"; String id3 = "partition-3"; String[] partitions = new String[]{PARTITION_ID, id2, id3}; + + // Set-up management node returns. EventHubManagementNode managementNode = mock(EventHubManagementNode.class); when(connection1.getManagementNode()).thenReturn(Mono.just(managementNode)); when(managementNode.getEventHubProperties()) @@ -530,12 +534,10 @@ public void receivesMultiplePartitions() { EmitterProcessor processor2 = EmitterProcessor.create(); FluxSink processor2sink = processor2.sink(); AmqpReceiveLink link2 = mock(AmqpReceiveLink.class); - EventHubSession session2 = mock(EventHubSession.class); EmitterProcessor processor3 = EmitterProcessor.create(); FluxSink processor3sink = processor3.sink(); AmqpReceiveLink link3 = mock(AmqpReceiveLink.class); - EventHubSession session3 = mock(EventHubSession.class); when(link2.receive()).thenReturn(processor2); when(link2.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); @@ -545,24 +547,19 @@ public void receivesMultiplePartitions() { when(link3.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); when(link3.getCredits()).thenReturn(numberOfEvents); - when(connection1.createSession(any())).thenAnswer(invocation -> { - String name = invocation.getArgument(0); - - if (name.endsWith(PARTITION_ID)) { - return Mono.just(session); - } else if (name.endsWith(id2)) { - return Mono.just(session2); - } else if (name.endsWith(id3)) { - return Mono.just(session3); - } else { - return Mono.error(new IllegalArgumentException("Unknown session: " + name)); - } - }); - when(session2.createConsumer(any(), argThat(name -> name.endsWith(id2)), any(), any(), any(), any())) - .thenReturn(Mono.just(link2)); - - when(session3.createConsumer(any(), argThat(name -> name.endsWith(id3)), any(), any(), any(), any())) - .thenReturn(Mono.just(link3)); + when(connection1.createReceiveLink(any(), anyString(), any(EventPosition.class), any(ReceiveOptions.class))) + .thenAnswer(mock -> { + String name = mock.getArgument(1); + if (name.endsWith(PARTITION_ID)) { + return Mono.just(amqpReceiveLink); + } else if (name.endsWith(id2)) { + return Mono.just(link2); + } else if (name.endsWith(id3)) { + return Mono.just(link3); + } else { + return Mono.error(new IllegalArgumentException("Unknown entityPath: " + name)); + } + }); // Act & Assert StepVerifier.create(asyncClient.receive(true).filter(e -> isMatchingEvent(e, messageTrackingUUID))) @@ -581,18 +578,24 @@ public void receivesMultiplePartitions() { * Verifies that even if one link closes, it still continues to receive. */ @Test - public void receivesMultiplePartitionsWhenOneCloses() { + void receivesMultiplePartitionsWhenOneCloses() { + // Arrange int numberOfEvents = 10; when(amqpReceiveLink.getCredits()).thenReturn(numberOfEvents); final FluxSink processor1sink = messageProcessor.sink(); EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + EventHubConnectionProcessor eventHubConnection = Flux.create(sink -> sink.next(connection1)) .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); + when(connection1.getEndpointStates()).thenReturn(endpointProcessor); + endpointSink.next(AmqpEndpointState.ACTIVE); + String id2 = "partition-2"; String id3 = "partition-3"; String[] partitions = new String[]{PARTITION_ID, id2, id3}; + + // Set-up management node returns. EventHubManagementNode managementNode = mock(EventHubManagementNode.class); when(connection1.getManagementNode()).thenReturn(Mono.just(managementNode)); when(managementNode.getEventHubProperties()) @@ -604,43 +607,32 @@ public void receivesMultiplePartitionsWhenOneCloses() { EmitterProcessor processor2 = EmitterProcessor.create(); FluxSink processor2sink = processor2.sink(); AmqpReceiveLink link2 = mock(AmqpReceiveLink.class); - EventHubSession session2 = mock(EventHubSession.class); EmitterProcessor processor3 = EmitterProcessor.create(); FluxSink processor3sink = processor3.sink(); AmqpReceiveLink link3 = mock(AmqpReceiveLink.class); - EventHubSession session3 = mock(EventHubSession.class); when(link2.receive()).thenReturn(processor2); - when(link2.getEndpointStates()).thenReturn(Flux.create(sink -> { - sink.next(AmqpEndpointState.ACTIVE); - })); + when(link2.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); when(link2.getCredits()).thenReturn(numberOfEvents); when(link3.receive()).thenReturn(processor3); - when(link3.getEndpointStates()).thenReturn(Flux.create(sink -> { - sink.next(AmqpEndpointState.ACTIVE); - })); + when(link3.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); when(link3.getCredits()).thenReturn(numberOfEvents); - when(connection1.createSession(any())).thenAnswer(invocation -> { - String name = invocation.getArgument(0); - - if (name.endsWith(PARTITION_ID)) { - return Mono.just(session); - } else if (name.endsWith(id2)) { - return Mono.just(session2); - } else if (name.endsWith(id3)) { - return Mono.just(session3); - } else { - return Mono.error(new IllegalArgumentException("Unknown session: " + name)); - } - }); - when(session2.createConsumer(any(), argThat(name -> name.endsWith(id2)), any(), any(), any(), any())) - .thenReturn(Mono.just(link2)); - - when(session3.createConsumer(any(), argThat(name -> name.endsWith(id3)), any(), any(), any(), any())) - .thenReturn(Mono.just(link3)); + when(connection1.createReceiveLink(any(), anyString(), any(EventPosition.class), any(ReceiveOptions.class))) + .thenAnswer(mock -> { + String name = mock.getArgument(1); + if (name.endsWith(PARTITION_ID)) { + return Mono.just(amqpReceiveLink); + } else if (name.endsWith(id2)) { + return Mono.just(link2); + } else if (name.endsWith(id3)) { + return Mono.just(link3); + } else { + return Mono.error(new IllegalArgumentException("Unknown session: " + name)); + } + }); // Act & Assert StepVerifier.create(asyncClient.receive(true).filter(e -> isMatchingEvent(e, messageTrackingUUID))) @@ -662,10 +654,10 @@ public void receivesMultiplePartitionsWhenOneCloses() { * Verifies that when we have a shared connection, the consumer does not close that connection. */ @Test - public void doesNotCloseSharedConnection() { + void doesNotCloseSharedConnection() { // Arrange EventHubAmqpConnection connection1 = mock(EventHubAmqpConnection.class); - EventHubConnectionProcessor eventHubConnection = Mono.fromCallable(() -> connection1) + EventHubConnectionProcessor eventHubConnection = Flux.create(sink -> sink.next(connection1)) .subscribeWith(new EventHubConnectionProcessor(HOSTNAME, EVENT_HUB_NAME, retryOptions)); EventHubConsumerAsyncClient sharedConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, eventHubConnection, messageSerializer, CONSUMER_GROUP, PREFETCH, true); @@ -674,14 +666,15 @@ public void doesNotCloseSharedConnection() { sharedConsumer.close(); // Verify - verify(eventHubConnection, never()).dispose(); + Assertions.assertFalse(eventHubConnection.isDisposed()); + verify(connection1, never()).close(); } /** * Verifies that when we have a non-shared connection, the consumer closes that connection. */ @Test - public void closesDedicatedConnection() { + void closesDedicatedConnection() { // Arrange EventHubConnectionProcessor hubConnection = mock(EventHubConnectionProcessor.class); EventHubConsumerAsyncClient dedicatedConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, From d44bfa677537a183542b81f531d044f06b94f29b Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 6 Jan 2020 18:40:52 -0800 Subject: [PATCH 22/31] Using parallel scheduler. --- .../com/azure/messaging/eventhubs/IntegrationTestBase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/IntegrationTestBase.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/IntegrationTestBase.java index de157d90a047..97e3a44a6c44 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/IntegrationTestBase.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/IntegrationTestBase.java @@ -52,7 +52,7 @@ public abstract class IntegrationTestBase extends TestBase { private ConnectionStringProperties properties; private String testName; - private final Scheduler scheduler = Schedulers.newParallel("eh-integration"); + private final Scheduler scheduler = Schedulers.parallel(); protected IntegrationTestBase(ClientLogger logger) { this.logger = logger; @@ -75,7 +75,6 @@ public void setupTest(TestInfo testInfo) { @AfterEach public void teardownTest(TestInfo testInfo) { logger.info("[{}]: Performing test clean-up.", testInfo.getDisplayName()); - scheduler.dispose(); afterTest(); // Tear down any inline mocks to avoid memory leaks. From 10737630e976b84917b3027395257e0d56adcec3 Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 6 Jan 2020 23:17:25 -0800 Subject: [PATCH 23/31] Delay retrying EventHubConnection creation on a transient error. --- .../com/azure/core/amqp/AmqpRetryPolicy.java | 4 +- .../EventHubConnectionProcessor.java | 46 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpRetryPolicy.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpRetryPolicy.java index 4a5a2c250b46..f086783a861a 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpRetryPolicy.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpRetryPolicy.java @@ -69,7 +69,7 @@ public int getMaxRetries() { * @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 calculateRetryDelay(Exception lastException, int retryCount) { + public Duration calculateRetryDelay(Throwable lastException, int retryCount) { if (retryOptions.getDelay() == Duration.ZERO || retryOptions.getMaxDelay() == Duration.ZERO || retryCount > retryOptions.getMaxRetries()) { @@ -138,7 +138,7 @@ public boolean equals(Object obj) { * @param exception An exception that was observed for the operation to be retried. * @return true if the exception is a retriable exception, otherwise false. */ - private static boolean isRetriableException(Exception exception) { + private static boolean isRetriableException(Throwable exception) { return (exception instanceof AmqpException) && ((AmqpException) exception).isTransient(); } } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 63bbd1731350..2e307a3cd0bd 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -3,19 +3,26 @@ package com.azure.messaging.eventhubs.implementation; +import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpRetryOptions; -import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.AmqpRetryPolicy; +import com.azure.core.amqp.implementation.RetryUtil; import com.azure.core.util.logging.ClientLogger; import org.reactivestreams.Processor; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.publisher.EmitterProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import java.time.Duration; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * Subscribes to an upstream Mono that creates {@link EventHubAmqpConnection} then publishes the created connection @@ -28,17 +35,22 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); + private final EmitterProcessor durationSource = EmitterProcessor.create(); + private final FluxSink durationSourceSink = durationSource.sink(FluxSink.OverflowStrategy.BUFFER); + private final AtomicInteger retryAttempts = new AtomicInteger(); private final Object lock = new Object(); private final String fullyQualifiedNamespace; private final String eventHubName; private final AmqpRetryOptions retryOptions; + private final AmqpRetryPolicy retryPolicy; + private final Disposable retrySubscription; private Subscription upstream; private EventHubAmqpConnection currentConnection; - private Disposable connectionSubscription; private volatile ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); private volatile Throwable lastError; + private volatile Disposable connectionSubscription; public EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventHubName, AmqpRetryOptions retryOptions) { @@ -46,6 +58,16 @@ public EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventH "'fullyQualifiedNamespace' cannot be null."); this.eventHubName = Objects.requireNonNull(eventHubName, "'eventHubName' cannot be null."); this.retryOptions = Objects.requireNonNull(retryOptions, "'retryOptions' cannot be null."); + + this.retryPolicy = RetryUtil.getRetryPolicy(retryOptions); + this.retrySubscription = Flux.switchOnNext(durationSource.map(duration -> Mono.delay(duration))) + .subscribe(unused -> { + if (upstream != null) { + upstream.request(1); + } else { + logger.info("No subscription to request."); + } + }); } /** @@ -101,6 +123,10 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { connectionSubscription = eventHubAmqpConnection.getEndpointStates().subscribe( state -> { + // Connection was successfully opened, we can reset the retry interval. + if (state == AmqpEndpointState.ACTIVE) { + retryAttempts.set(0); + } }, error -> onError(error), () -> { @@ -128,9 +154,14 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { public void onError(Throwable throwable) { Objects.requireNonNull(throwable, "'throwable' is required."); - if (throwable instanceof AmqpException && ((AmqpException) throwable).isTransient()) { - logger.warning("Transient occurred in connection. Retrying.", throwable); - upstream.request(1); + final int attempt = retryAttempts.incrementAndGet(); + final Duration retryInterval = retryPolicy.calculateRetryDelay(throwable, attempt); + + if (retryInterval != null) { + logger.warning("Transient error occurred. Attempt: {}. Retrying after {} seconds.", + attempt, retryInterval.toSeconds(), throwable); + + durationSourceSink.next(retryInterval); return; } @@ -196,6 +227,11 @@ public void subscribe(CoreSubscriber actual) { @Override public void dispose() { + if (isTerminated.getAndSet(true)) { + return; + } + + retrySubscription.dispose(); onComplete(); synchronized (lock) { From 2f74cacd783302287e68653f0320bb63f1f1f761 Mon Sep 17 00:00:00 2001 From: Connie Date: Mon, 6 Jan 2020 23:24:12 -0800 Subject: [PATCH 24/31] Update to .toMillis() --- .../eventhubs/implementation/EventHubConnectionProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 2e307a3cd0bd..c12491e6ed6c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -158,8 +158,8 @@ public void onError(Throwable throwable) { final Duration retryInterval = retryPolicy.calculateRetryDelay(throwable, attempt); if (retryInterval != null) { - logger.warning("Transient error occurred. Attempt: {}. Retrying after {} seconds.", - attempt, retryInterval.toSeconds(), throwable); + logger.warning("Transient error occurred. Attempt: {}. Retrying after {} ms.", + attempt, retryInterval.toMillis(), throwable); durationSourceSink.next(retryInterval); return; From 526f996e88c6597c01c2bb1d3d9e1f026205e389 Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 00:26:55 -0800 Subject: [PATCH 25/31] Setting retry options. --- .../eventhubs/EventHubProducerAsyncClientTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java index 95342d200e2f..946e759e2c2a 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java @@ -4,6 +4,7 @@ package com.azure.messaging.eventhubs; import com.azure.core.amqp.AmqpEndpointState; +import com.azure.core.amqp.AmqpRetryMode; import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpTransportType; import com.azure.core.amqp.ProxyOptions; @@ -91,7 +92,10 @@ class EventHubProducerAsyncClientTest { private ArgumentCaptor> messagesCaptor; private final MessageSerializer messageSerializer = new EventHubMessageSerializer(); - private final AmqpRetryOptions retryOptions = new AmqpRetryOptions().setTryTimeout(Duration.ofSeconds(10)); + private final AmqpRetryOptions retryOptions = new AmqpRetryOptions() + .setDelay(Duration.ofMillis(500)) + .setMode(AmqpRetryMode.FIXED) + .setTryTimeout(Duration.ofSeconds(10)); private final DirectProcessor endpointProcessor = DirectProcessor.create(); private final FluxSink endpointSink = endpointProcessor.sink(FluxSink.OverflowStrategy.BUFFER); private EventHubProducerAsyncClient producer; From ca903dc1afa25685639da1afc605c5c876905638 Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 00:27:52 -0800 Subject: [PATCH 26/31] Complete subscriber. --- .../implementation/EventHubConnectionProcessor.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index c12491e6ed6c..9db95f1e23a4 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -128,7 +128,12 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { retryAttempts.set(0); } }, - error -> onError(error), + error -> { + synchronized (lock) { + currentConnection = null; + } + onError(error); + }, () -> { if (isDisposed()) { logger.info("Connection is disposed. Not requesting another connection."); @@ -277,8 +282,7 @@ public void onComplete() { @Override public void onNext(EventHubAmqpConnection connection) { if (!isCancelled()) { - super.onNext(connection); - actual.onNext(connection); + super.complete(connection); } } From 079dbdbccc6226404be321ba17d870153ec2b5bd Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 01:04:10 -0800 Subject: [PATCH 27/31] Consolidate request from upstream for new connection. --- .../EventHubConnectionProcessor.java | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 9db95f1e23a4..df5a3d1c2ec7 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -12,9 +12,6 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; -import reactor.core.publisher.EmitterProcessor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; @@ -35,15 +32,12 @@ public class EventHubConnectionProcessor extends Mono private final ClientLogger logger = new ClientLogger(EventHubConnectionProcessor.class); private final AtomicBoolean isTerminated = new AtomicBoolean(); private final AtomicBoolean isRequested = new AtomicBoolean(); - private final EmitterProcessor durationSource = EmitterProcessor.create(); - private final FluxSink durationSourceSink = durationSource.sink(FluxSink.OverflowStrategy.BUFFER); private final AtomicInteger retryAttempts = new AtomicInteger(); private final Object lock = new Object(); private final String fullyQualifiedNamespace; private final String eventHubName; private final AmqpRetryOptions retryOptions; private final AmqpRetryPolicy retryPolicy; - private final Disposable retrySubscription; private Subscription upstream; private EventHubAmqpConnection currentConnection; @@ -51,6 +45,7 @@ public class EventHubConnectionProcessor extends Mono private volatile ConcurrentLinkedDeque subscribers = new ConcurrentLinkedDeque<>(); private volatile Throwable lastError; private volatile Disposable connectionSubscription; + private volatile Disposable retrySubscription; public EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventHubName, AmqpRetryOptions retryOptions) { @@ -60,14 +55,6 @@ public EventHubConnectionProcessor(String fullyQualifiedNamespace, String eventH this.retryOptions = Objects.requireNonNull(retryOptions, "'retryOptions' cannot be null."); this.retryPolicy = RetryUtil.getRetryPolicy(retryOptions); - this.retrySubscription = Flux.switchOnNext(durationSource.map(duration -> Mono.delay(duration))) - .subscribe(unused -> { - if (upstream != null) { - upstream.request(1); - } else { - logger.info("No subscription to request."); - } - }); } /** @@ -139,7 +126,7 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { logger.info("Connection is disposed. Not requesting another connection."); } else { logger.info("Connection closed. Requesting another."); - upstream.request(1); +// upstream.request(1); } }); } @@ -166,7 +153,10 @@ public void onError(Throwable throwable) { logger.warning("Transient error occurred. Attempt: {}. Retrying after {} ms.", attempt, retryInterval.toMillis(), throwable); - durationSourceSink.next(retryInterval); + retrySubscription = Mono.delay(retryInterval).subscribe(i -> { + requestUpstream(); + }); + return; } @@ -220,14 +210,10 @@ public void subscribe(CoreSubscriber actual) { subscriber.complete(currentConnection); return; } - - subscribers.add(subscriber); - - if (!isRequested.getAndSet(true)) { - logger.info("Connection not obtained yet. Requesting one."); - upstream.request(1); - } } + + subscribers.add(subscriber); + requestUpstream(); } @Override @@ -236,7 +222,10 @@ public void dispose() { return; } - retrySubscription.dispose(); + if (retrySubscription != null && !retrySubscription.isDisposed()) { + retrySubscription.dispose(); + } + onComplete(); synchronized (lock) { @@ -253,6 +242,26 @@ public boolean isDisposed() { return isTerminated.get(); } + private void requestUpstream() { + synchronized (lock) { + if (currentConnection != null) { + logger.info("Connection exists, not requesting another."); + return; + } else if (isTerminated.get() || upstream == null) { + logger.verbose("Terminated. Not requesting another."); + return; + } + } + + // subscribe(CoreSubscriber) may have requested a subscriber already. + if (!isRequested.getAndSet(true)) { + logger.info("Connection not requested, yet. Requesting one."); + upstream.request(1); + } else { + logger.info("Retried connection already requested."); + } + } + /** * Represents a subscriber, waiting for an AMQP connection. */ From 38fe3a56f44616622dda3227c2f99454b530b054 Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 01:13:09 -0800 Subject: [PATCH 28/31] Fix unit tests. --- .../eventhubs/EventHubConsumerAsyncClientTest.java | 3 --- .../eventhubs/EventHubConsumerClientTest.java | 14 ++++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java index e9e829e85b4b..cd1010f909de 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java @@ -16,7 +16,6 @@ import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; -import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; import com.azure.messaging.eventhubs.models.LastEnqueuedEventProperties; import com.azure.messaging.eventhubs.models.PartitionEvent; @@ -94,8 +93,6 @@ class EventHubConsumerAsyncClientTest { @Mock private EventHubAmqpConnection connection; @Mock - private EventHubSession session; - @Mock private TokenCredential tokenCredential; @Captor diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java index 950b66ed81a2..a087cda91d2b 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java @@ -3,6 +3,7 @@ package com.azure.messaging.eventhubs; +import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpTransportType; import com.azure.core.amqp.ProxyOptions; @@ -14,7 +15,6 @@ import com.azure.core.util.IterableStream; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; -import com.azure.messaging.eventhubs.implementation.EventHubSession; import com.azure.messaging.eventhubs.models.EventPosition; import com.azure.messaging.eventhubs.models.LastEnqueuedEventProperties; import com.azure.messaging.eventhubs.models.PartitionEvent; @@ -28,6 +28,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -72,6 +73,8 @@ public class EventHubConsumerClientTest { private final FluxSink sink = messageProcessor.sink(FluxSink.OverflowStrategy.BUFFER); private final MessageSerializer messageSerializer = new EventHubMessageSerializer(); + private final DirectProcessor endpointProcessor = DirectProcessor.create(); + @Mock private AmqpReceiveLink amqpReceiveLink; @Mock @@ -79,8 +82,7 @@ public class EventHubConsumerClientTest { @Mock private EventHubAmqpConnection connection; - @Mock - private EventHubSession session; + @Mock private TokenCredential tokenCredential; @@ -104,9 +106,9 @@ CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS connectionProcessor = Mono.fromCallable(() -> connection).subscribeWith(new EventHubConnectionProcessor( connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), connectionOptions.getRetry())); - when(connection.createSession(argThat(name -> name.endsWith(PARTITION_ID)))) - .thenReturn(Mono.fromCallable(() -> session)); - when(session.createConsumer(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(), any(), any(), any())) + + when(connection.getEndpointStates()).thenReturn(endpointProcessor); + when(connection.createReceiveLink(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(EventPosition.class), any(ReceiveOptions.class))) .thenReturn(Mono.just(amqpReceiveLink), Mono.fromCallable(() -> { System.out.println("Returning second link"); return amqpReceiveLink2; From 27614c58b37216aa83e951ec7c806e502a48f79b Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 01:23:47 -0800 Subject: [PATCH 29/31] Not requesting new connection unless needed. --- .../implementation/EventHubConnectionProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index df5a3d1c2ec7..374017d6cbea 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -123,10 +123,10 @@ public void onNext(EventHubAmqpConnection eventHubAmqpConnection) { }, () -> { if (isDisposed()) { - logger.info("Connection is disposed. Not requesting another connection."); + logger.info("Connection is disposed."); } else { - logger.info("Connection closed. Requesting another."); -// upstream.request(1); + logger.info("Connection closed."); + currentConnection = null; } }); } From cd404de02877c94492cd90e23542627b0db8bfa8 Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 10:41:15 -0800 Subject: [PATCH 30/31] Fixing tests. --- ...HubConsumerAsyncClientIntegrationTest.java | 2 +- .../eventhubs/EventHubConsumerClientTest.java | 6 +- ...HubProducerAsyncClientIntegrationTest.java | 44 ++++++++++++- .../eventhubs/EventHubProducerClientTest.java | 61 ++++++------------- 4 files changed, 66 insertions(+), 47 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientIntegrationTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientIntegrationTest.java index e1e6eaa19774..50f742d21fc0 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientIntegrationTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientIntegrationTest.java @@ -386,7 +386,7 @@ public void canReceive() { } finally { isActive.set(false); producerEvents.dispose(); - consumer.close(); + dispose(producer, consumer); } } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java index a087cda91d2b..6128c8e5cd07 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java @@ -103,9 +103,9 @@ public void setup() { connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - connectionProcessor = Mono.fromCallable(() -> connection).subscribeWith(new EventHubConnectionProcessor( - connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), - connectionOptions.getRetry())); + connectionProcessor = Flux.create(sink -> sink.next(connection)) + .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); when(connection.getEndpointStates()).thenReturn(endpointProcessor); when(connection.createReceiveLink(any(), argThat(name -> name.endsWith(PARTITION_ID)), any(EventPosition.class), any(ReceiveOptions.class))) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java index de95c04f17e9..9fb235431a24 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java @@ -3,17 +3,23 @@ package com.azure.messaging.eventhubs; +import com.azure.core.amqp.exception.AmqpException; import com.azure.core.util.logging.ClientLogger; import com.azure.messaging.eventhubs.models.CreateBatchOptions; import com.azure.messaging.eventhubs.models.SendOptions; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; import static java.nio.charset.StandardCharsets.UTF_8; @@ -155,10 +161,12 @@ public void sendAllPartitions() { Assertions.assertNotNull(partitionIds); for (String partitionId : partitionIds) { - final EventDataBatch batch = producer.createBatch(new CreateBatchOptions().setPartitionId(partitionId)).block(TIMEOUT); + final EventDataBatch batch = + producer.createBatch(new CreateBatchOptions().setPartitionId(partitionId)).block(TIMEOUT); Assertions.assertNotNull(batch); - Assertions.assertTrue(batch.tryAdd(TestUtils.getEvent("event", "test guid", Integer.parseInt(partitionId)))); + Assertions.assertTrue(batch.tryAdd(TestUtils.getEvent("event", "test guid", + Integer.parseInt(partitionId)))); // Act & Assert StepVerifier.create(producer.send(batch)).expectComplete().verify(TIMEOUT); @@ -193,4 +201,36 @@ public void sendWithCredentials() { dispose(client); } } + + @Disabled("Testing long running operations and disconnections.") + @Test + void worksAfterReconnection() throws InterruptedException { + Flux.interval(Duration.ofSeconds(5)) + .flatMap(position -> producer.createBatch().flatMap(batch -> { + IntStream.range(0, 3).mapToObj(number -> new EventData("Position" + position + ": " + number)) + .forEach(event -> { + if (!batch.tryAdd(event)) { + logger.error("Could not add event. Size: {}. Max: {}. Content: {}", + batch.getSizeInBytes(), batch.getMaxSizeInBytes(), event.getBodyAsString()); + } + }); + + return producer.send(batch).thenReturn(Instant.now()); + })) + .onErrorContinue(error -> error instanceof AmqpException && ((AmqpException) error).isTransient(), + (error, value) -> { + System.out.println("Exception dropped: " + error.getMessage()); + }) + .subscribe(instant -> { + System.out.println("Sent batch at: " + instant); + }, error -> { + logger.error("Error sending batch: ", error); + }, () -> { + logger.info("Complete."); + }); + + System.out.println("Sleeping while performing work."); + TimeUnit.MINUTES.sleep(30); + System.out.println("Complete."); + } } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java index be5f1d2611d3..a6a69590aee5 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java @@ -3,8 +3,8 @@ package com.azure.messaging.eventhubs; +import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpRetryOptions; -import com.azure.core.amqp.AmqpSession; import com.azure.core.amqp.AmqpTransportType; import com.azure.core.amqp.ProxyOptions; import com.azure.core.amqp.exception.AmqpErrorCondition; @@ -69,8 +69,6 @@ public class EventHubProducerClientTest { @Mock private AmqpSendLink sendLink; @Mock - private AmqpSession session; - @Mock private EventHubAmqpConnection connection; @Mock private TokenCredential tokenCredential; @@ -97,11 +95,13 @@ public void setup() { ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, "event-hub-path", tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel()); - connectionProcessor = Mono.fromCallable(() -> connection).subscribeWith(new EventHubConnectionProcessor( - connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getEntityPath(), - connectionOptions.getRetry())); + connectionProcessor = Flux.create(sink -> sink.next(connection)) + .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), + connectionOptions.getEntityPath(), connectionOptions.getRetry())); asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, retryOptions, tracerProvider, messageSerializer, false); + + when(connection.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); } @AfterEach @@ -121,11 +121,8 @@ public void sendSingleMessage() { final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); final EventData eventData = new EventData("hello-world".getBytes(UTF_8)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(sendLink)); // Act @@ -153,11 +150,8 @@ public void sendStartSpanSingleMessage() { final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); final EventData eventData = new EventData("hello-world".getBytes(UTF_8)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(sendLink)); when(tracer1.start(eq("EventHubs.send"), any(), eq(ProcessKind.SEND))).thenAnswer( @@ -200,11 +194,8 @@ public void sendMessageRetrySpanTest() { final List tracers = Collections.singletonList(tracer1); TracerProvider tracerProvider = new TracerProvider(tracers); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(sendLink)); final EventHubProducerAsyncClient asyncProducer = new EventHubProducerAsyncClient(HOSTNAME, EVENT_HUB_NAME, @@ -253,11 +244,9 @@ public void sendMultipleMessages() { final SendOptions options = new SendOptions().setPartitionId(partitionId); final EventHubProducerClient producer = new EventHubProducerClient(asyncProducer, retryOptions.getTryTimeout()); - when(connection.createSession(argThat(name -> name.endsWith(partitionId)))) - .thenReturn(Mono.just(session)); - - when(session.createProducer(argThat(name -> name.startsWith("PS")), argThat(name -> name.endsWith(partitionId)), - eq(retryOptions.getTryTimeout()), any())) + // EC is the prefix they use when creating a link that sends to the service round-robin. + when(connection.createSendLink(argThat(name -> name.startsWith("PS")), + argThat(name -> name.endsWith(partitionId)), any())) .thenReturn(Mono.just(sendLink)); // Act @@ -287,11 +276,9 @@ public void createsEventDataBatch() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(maxLinkSize)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(link)); // This event is 1024 bytes when serialized. @@ -328,12 +315,10 @@ public void startsMessageSpanOnEventBatch() { final AmqpSendLink link = mock(AmqpSendLink.class); when(link.getLinkSize()).thenReturn(Mono.just(ClientConstants.MAX_MESSAGE_LENGTH_BYTES)); - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) - .thenReturn(Mono.just(link)); + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) + .thenReturn(Mono.just(sendLink)); when(tracer1.start(eq("EventHubs.message"), any(), eq(ProcessKind.MESSAGE))).thenAnswer( invocation -> { @@ -364,11 +349,8 @@ public void createsEventDataBatchWithPartitionKey() { int eventOverhead = 98; int maxEventPayload = maxBatchSize - eventOverhead; - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(sendLink)); // This event is 1024 bytes when serialized. @@ -401,11 +383,10 @@ public void createsEventDataBatchWithPartitionId() { int maxEventPayload = maxBatchSize - eventOverhead; String partitionId = "my-partition-id"; - when(connection.createSession(argThat(name -> name.endsWith(partitionId)))).thenReturn(Mono.just(session)); // PS is the prefix when a partition sender link is created. - when(session.createProducer(argThat(name -> name.startsWith("PS")), argThat(name -> name.endsWith(partitionId)), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("PS")), + argThat(name -> name.endsWith(partitionId)), any())) .thenReturn(Mono.just(sendLink)); // This event is 1024 bytes when serialized. @@ -437,13 +418,11 @@ public void payloadTooLarge() { int eventOverhead = 24; int maxEventPayload = maxBatchSize - eventOverhead; - when(connection.createSession(EVENT_HUB_NAME)).thenReturn(Mono.just(session)); - // EC is the prefix they use when creating a link that sends to the service round-robin. - when(session.createProducer(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), - eq(retryOptions.getTryTimeout()), any())) + when(connection.createSendLink(argThat(name -> name.startsWith("EC")), eq(EVENT_HUB_NAME), any())) .thenReturn(Mono.just(sendLink)); + // This event is 1025 bytes when serialized. final EventData event = new EventData(new byte[maxEventPayload + 1]); From 7dff1595e66f89b2428a6c63fb1f4d3655dfd598 Mon Sep 17 00:00:00 2001 From: Connie Date: Tue, 7 Jan 2020 11:21:46 -0800 Subject: [PATCH 31/31] Fixing checkstyle issues. --- .../implementation/EventHubConnectionProcessor.java | 2 +- .../EventHubProducerAsyncClientIntegrationTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java index 374017d6cbea..e7e206bd1479 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessor.java @@ -265,7 +265,7 @@ private void requestUpstream() { /** * Represents a subscriber, waiting for an AMQP connection. */ - private static class ConnectionSubscriber + private static final class ConnectionSubscriber extends Operators.MonoSubscriber { private final EventHubConnectionProcessor processor; diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java index 9fb235431a24..39514947940e 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientIntegrationTest.java @@ -224,10 +224,10 @@ void worksAfterReconnection() throws InterruptedException { .subscribe(instant -> { System.out.println("Sent batch at: " + instant); }, error -> { - logger.error("Error sending batch: ", error); - }, () -> { - logger.info("Complete."); - }); + logger.error("Error sending batch: ", error); + }, () -> { + logger.info("Complete."); + }); System.out.println("Sleeping while performing work."); TimeUnit.MINUTES.sleep(30);