Skip to content

Sketch Environment/Producer/Consumer API #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
93cd259
Sketch producer API
acogoluegnes Jul 9, 2020
8c9d2b7
Merge branch 'master' into producer-spike
acogoluegnes Jul 10, 2020
ba18b3b
Introduce Environment API
acogoluegnes Jul 15, 2020
1087abf
Add URI(s) parameters to build Environment
acogoluegnes Jul 17, 2020
e8f874d
Merge branch 'master' into producer-spike
acogoluegnes Jul 17, 2020
18390f6
Remove some publish methods in Client
acogoluegnes Jul 17, 2020
96dfc24
Organize classes between API/implementation packages
acogoluegnes Jul 17, 2020
be54e5c
Introduce Consumer API
acogoluegnes Jul 20, 2020
80f64e9
Handle publishing error in producer
acogoluegnes Jul 20, 2020
7baa62c
Support stream creation/deletion in environment
acogoluegnes Jul 20, 2020
bc5e567
Document Environment API
acogoluegnes Jul 21, 2020
c6b2d68
Document Producer API
acogoluegnes Jul 21, 2020
d8cc437
Document Consumer API
acogoluegnes Jul 21, 2020
91f52ab
Publish temp API documentation
acogoluegnes Jul 21, 2020
c5c45e2
Support sub-entry batching in producer
acogoluegnes Jul 27, 2020
d8cf412
Synchronize message accumulator access
acogoluegnes Jul 27, 2020
afb7203
Limit number of outstanding publish confirms
acogoluegnes Jul 27, 2020
ea960d0
Document maxUnconfirmedMessages and subEntrySize
acogoluegnes Jul 27, 2020
07f8681
Recover locator connection in environment
acogoluegnes Jul 28, 2020
504a68b
Close producers and consumers in environment
acogoluegnes Jul 28, 2020
7d3bea5
Document environment settings
acogoluegnes Jul 29, 2020
9e2300e
Use new API for sample application
acogoluegnes Jul 29, 2020
a0771a2
Use Consumer in performance tool
acogoluegnes Jul 29, 2020
147b8d5
Downsample latency calculation in performance tool
acogoluegnes Jul 29, 2020
b41aa94
Deal with stream unavailibility in consumer
acogoluegnes Jul 30, 2020
4bc6744
Add delay before consumer re-assignment after metadata update
acogoluegnes Jul 31, 2020
87b866d
Add unit tests for DefaultClientSubscriptions
acogoluegnes Jul 31, 2020
c5e3736
Unit test DefaultClientSubscriptions sub/unsub
acogoluegnes Jul 31, 2020
a4e5abe
Update data structure before subscription
acogoluegnes Jul 31, 2020
eae2887
More DefaultClientSubscriptions unit tests
acogoluegnes Jul 31, 2020
6782fa8
More DefaultClientSubscriptions unit tests
acogoluegnes Jul 31, 2020
cd5cf2f
Schedule candidates lookup on metadata update
acogoluegnes Aug 3, 2020
cc0806e
Create async retry utility for metadata update
acogoluegnes Aug 3, 2020
f0ded28
More DefaultClientSubscriptions unit tests
acogoluegnes Aug 3, 2020
c4fea33
Use async retry utility for locator recovery
acogoluegnes Aug 3, 2020
53ea1d9
Add some Environment unit tests
acogoluegnes Aug 4, 2020
5efe862
Handle connection loss in consumer
acogoluegnes Aug 5, 2020
83c3c12
Rename RecoveryBackOffDelayPolicy to BackOffDelayPolicy
acogoluegnes Aug 5, 2020
fe91c97
Add node failure test for consumer
acogoluegnes Aug 6, 2020
6afab81
Add unit for consumer connection recovery
acogoluegnes Aug 6, 2020
ea52a49
Disable a couple of recovery tests
acogoluegnes Aug 6, 2020
a6bea16
Remove Client documentation
acogoluegnes Aug 6, 2020
b3d4bc0
Improve wording in documentation
acogoluegnes Aug 6, 2020
0190f1d
Add some Javadoc to Client
acogoluegnes Aug 6, 2020
a2f2394
Kill connection instead of stopping node in test
acogoluegnes Aug 6, 2020
298826e
Publish documentation to snapshot directory
acogoluegnes Aug 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More DefaultClientSubscriptions unit tests
  • Loading branch information
acogoluegnes committed Jul 31, 2020
commit eae2887dfc17ed953deba0ecea419fe9a572e8e2
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@

class DefaultClientSubscriptions implements ClientSubscriptions {

static final Duration DELAY_AFTER_METADATA_UPDATE = Duration.ofSeconds(5);
static final Duration DEFAULT_DELAY_AFTER_METADATA_UPDATE = Duration.ofSeconds(5);
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClientSubscriptions.class);
private final Random random = new Random();
private final AtomicLong globalSubscriptionIdSequence = new AtomicLong(0);
private final StreamEnvironment environment;
private final Map<String, SubscriptionState> clientSubscriptionStates = new ConcurrentHashMap<>();
private final Map<Long, StreamSubscription> streamSubscriptionRegistry = new ConcurrentHashMap<>();
private final Function<Client.ClientParameters, Client> clientFactory;
volatile Duration delayAfterMetadataUpdate = DEFAULT_DELAY_AFTER_METADATA_UPDATE;

DefaultClientSubscriptions(StreamEnvironment environment, Function<Client.ClientParameters, Client> clientFactory) {
this.environment = environment;
Expand Down Expand Up @@ -180,7 +181,7 @@ private class SubscriptionState {
private SubscriptionState(Client.ClientParameters clientParameters) {
this.client = clientFactory.apply(clientParameters
.chunkListener((client, subscriptionId, offset, messageCount, dataSize) -> client.credit(subscriptionId, 1))
.creditNotification((subscriptionId, responseCode) -> LOGGER.debug("Received notification for subscription {}: {}", subscriptionId, responseCode))
.creditNotification((subscriptionId, responseCode) -> LOGGER.debug("Received credit notification for subscription {}: {}", subscriptionId, responseCode))
.messageListener((subscriptionId, offset, message) -> {
StreamSubscription streamSubscription = streamSubscriptions.get(subscriptionId);
if (streamSubscription != null) {
Expand Down Expand Up @@ -250,29 +251,33 @@ private SubscriptionState(Client.ClientParameters clientParameters) {
consumersClosingCallback.run();
} else {
for (StreamSubscription affectedSubscription : affectedSubscriptions) {
Client.Broker broker = pickBroker(candidates);
LOGGER.debug("Using {} to resume consuming from {}", broker, stream);
String key = keyForClientSubscriptionState(broker);
// FIXME in case the broker is no longer there, we may have to deal with an error here
SubscriptionState subscriptionState = clientSubscriptionStates.computeIfAbsent(key, s -> new SubscriptionState(environment
.clientParametersCopy()
.host(broker.getHost())
.port(broker.getPort())
));
if (affectedSubscription.consumer.isOpen()) {
synchronized (affectedSubscription.consumer) {
if (affectedSubscription.consumer.isOpen()) {
subscriptionState.add(affectedSubscription, OffsetSpecification.offset(affectedSubscription.offset));
try {
Client.Broker broker = pickBroker(candidates);
LOGGER.debug("Using {} to resume consuming from {}", broker, stream);
String key = keyForClientSubscriptionState(broker);
// FIXME in case the broker is no longer there, we may have to deal with an error here
SubscriptionState subscriptionState = clientSubscriptionStates.computeIfAbsent(key, s -> new SubscriptionState(environment
.clientParametersCopy()
.host(broker.getHost())
.port(broker.getPort())
));
if (affectedSubscription.consumer.isOpen()) {
synchronized (affectedSubscription.consumer) {
if (affectedSubscription.consumer.isOpen()) {
subscriptionState.add(affectedSubscription, OffsetSpecification.offset(affectedSubscription.offset));
}
}
}
} catch (Exception e) {
LOGGER.warn("Error while re-assigning subscription from stream {}", stream, e.getMessage());
}

}
}

}
}, DELAY_AFTER_METADATA_UPDATE.toMillis(), TimeUnit.MILLISECONDS);
}, delayAfterMetadataUpdate.toMillis(), TimeUnit.MILLISECONDS);
}

}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@
import com.rabbitmq.stream.OffsetSpecification;
import com.rabbitmq.stream.StreamDoesNotExistException;
import com.rabbitmq.stream.codec.WrapperMessageBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -46,25 +50,54 @@ public class DefaultClientSubscriptionsTest {
Client locator;
@Mock
Function<Client.ClientParameters, Client> clientFactory;
@Mock
Client client;
@Captor
ArgumentCaptor<Integer> subscriptionIdCaptor;

DefaultClientSubscriptions clientSubscriptions;
ScheduledExecutorService scheduledExecutorService;
Client.ClientParameters clientParameters;
volatile Client.MetadataListener metadataListener;
volatile Client.MessageListener messageListener;

@BeforeEach
void init() {
this.clientParameters = new Client.ClientParameters() {
@Override
public Client.ClientParameters metadataListener(Client.MetadataListener metadataListener) {
DefaultClientSubscriptionsTest.this.metadataListener = metadataListener;
return super.metadataListener(metadataListener);
}

@Override
public Client.ClientParameters messageListener(Client.MessageListener messageListener) {
DefaultClientSubscriptionsTest.this.messageListener = messageListener;
return super.messageListener(messageListener);
}
};
MockitoAnnotations.initMocks(this);
when(environment.locator()).thenReturn(locator);
when(environment.clientParametersCopy()).thenReturn(clientParameters);

clientSubscriptions = new DefaultClientSubscriptions(environment, clientFactory);
}

@AfterEach
void tearDown() {
if (scheduledExecutorService != null) {
scheduledExecutorService.shutdownNow();
}
}

@Test
void subscribeShouldThrowExceptionWhenNoMetadataForTheStream() {
when(environment.locator()).thenReturn(locator);
assertThatThrownBy(() -> clientSubscriptions.subscribe(consumer, "stream", OffsetSpecification.first(), (offset, message) -> {
})).isInstanceOf(StreamDoesNotExistException.class);
}

@Test
void subscribeShouldThrowExceptionWhenStreamDoesNotExist() {
when(environment.locator()).thenReturn(locator);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_STREAM_DOES_NOT_EXIST, null, null)));
Expand All @@ -74,7 +107,6 @@ void subscribeShouldThrowExceptionWhenStreamDoesNotExist() {

@Test
void subscribeShouldThrowExceptionWhenMetadataResponseIsNotOk() {
when(environment.locator()).thenReturn(locator);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_ACCESS_REFUSED, null, null)));
Expand All @@ -84,7 +116,6 @@ void subscribeShouldThrowExceptionWhenMetadataResponseIsNotOk() {

@Test
void subscribeShouldThrowExceptionIfNodeAvailableForStream() {
when(environment.locator()).thenReturn(locator);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_OK, null, null)));
Expand All @@ -94,7 +125,6 @@ void subscribeShouldThrowExceptionIfNodeAvailableForStream() {

@Test
void findBrokersForStreamShouldReturnLeaderIfNoReplicas() {
when(environment.locator()).thenReturn(locator);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_OK, leader(), null)));
Expand All @@ -105,7 +135,6 @@ void findBrokersForStreamShouldReturnLeaderIfNoReplicas() {

@Test
void findBrokersForStreamShouldReturnReplicasIfThereAreSome() {
when(environment.locator()).thenReturn(locator);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_OK, null, replicas())));
Expand All @@ -116,22 +145,10 @@ void findBrokersForStreamShouldReturnReplicasIfThereAreSome() {

@Test
void subscribeShouldSubscribeToStreamAndDispatchesMessage_UnsubscribeShouldUnsubscribe() {
AtomicReference<Client.MessageListener> messageListenerReference = new AtomicReference<>();
Client.ClientParameters cp = new Client.ClientParameters() {
@Override
public Client.ClientParameters messageListener(Client.MessageListener messageListener) {
messageListenerReference.set(messageListener);
return super.messageListener(messageListener);
}
};
when(environment.locator()).thenReturn(locator);
when(environment.clientParametersCopy()).thenReturn(cp);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_OK, null, replicas())));

Client client = mock(Client.class);
ArgumentCaptor<Integer> subscriptionIdCaptor = ArgumentCaptor.forClass(Integer.class);
when(clientFactory.apply(any(Client.ClientParameters.class))).thenReturn(client);
when(client.subscribe(subscriptionIdCaptor.capture(), anyString(), any(OffsetSpecification.class), anyInt()))
.thenReturn(new Client.Response(Constants.RESPONSE_CODE_OK));
Expand All @@ -144,7 +161,7 @@ public Client.ClientParameters messageListener(Client.MessageListener messageLis
verify(client, times(1)).subscribe(anyInt(), anyString(), any(OffsetSpecification.class), anyInt());

assertThat(messageHandlerCalls.get()).isEqualTo(0);
messageListenerReference.get().handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
messageListener.handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
assertThat(messageHandlerCalls.get()).isEqualTo(1);

when(client.unsubscribe(subscriptionIdCaptor.getValue()))
Expand All @@ -153,10 +170,55 @@ public Client.ClientParameters messageListener(Client.MessageListener messageLis
clientSubscriptions.unsubscribe(subscriptionGlobalId);
verify(client, times(1)).unsubscribe(subscriptionIdCaptor.getValue());

messageListenerReference.get().handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
messageListener.handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
assertThat(messageHandlerCalls.get()).isEqualTo(1);
}

@Test
void shouldRedistributeConsumerOnMetadataUpdate() throws Exception {
clientSubscriptions.delayAfterMetadataUpdate = Duration.ofMillis(500);
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
when(environment.scheduledExecutorService()).thenReturn(scheduledExecutorService);
when(consumer.isOpen()).thenReturn(true);
when(locator.metadata("stream"))
.thenReturn(Collections.singletonMap("stream",
new Client.StreamMetadata("stream", Constants.RESPONSE_CODE_OK, null, replicas())));

when(clientFactory.apply(any(Client.ClientParameters.class))).thenReturn(client);
when(client.subscribe(subscriptionIdCaptor.capture(), anyString(), any(OffsetSpecification.class), anyInt()))
.thenReturn(new Client.Response(Constants.RESPONSE_CODE_OK));

AtomicInteger messageHandlerCalls = new AtomicInteger();
long subscriptionGlobalId = clientSubscriptions.subscribe(consumer, "stream", OffsetSpecification.first(), (offset, message) -> {
messageHandlerCalls.incrementAndGet();
});
verify(clientFactory, times(1)).apply(any(Client.ClientParameters.class));
verify(client, times(1)).subscribe(anyInt(), anyString(), any(OffsetSpecification.class), anyInt());

assertThat(messageHandlerCalls.get()).isEqualTo(0);
messageListener.handle(subscriptionIdCaptor.getValue(), 1, new WrapperMessageBuilder().build());
assertThat(messageHandlerCalls.get()).isEqualTo(1);

metadataListener.handle("stream", Constants.RESPONSE_CODE_STREAM_NOT_AVAILABLE);

Thread.sleep(clientSubscriptions.delayAfterMetadataUpdate.toMillis() * 2);

verify(client, times(2)).subscribe(anyInt(), anyString(), any(OffsetSpecification.class), anyInt());

assertThat(messageHandlerCalls.get()).isEqualTo(1);
messageListener.handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
assertThat(messageHandlerCalls.get()).isEqualTo(2);

when(client.unsubscribe(subscriptionIdCaptor.getValue()))
.thenReturn(new Client.Response(Constants.RESPONSE_CODE_OK));

clientSubscriptions.unsubscribe(subscriptionGlobalId);
verify(client, times(1)).unsubscribe(subscriptionIdCaptor.getValue());

messageListener.handle(subscriptionIdCaptor.getValue(), 0, new WrapperMessageBuilder().build());
assertThat(messageHandlerCalls.get()).isEqualTo(2);
}

Client.Broker leader() {
return new Client.Broker("leader", -1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ void consumerShouldKeepConsumingIfStreamBecomesUnavailable() throws Exception {
Host.rabbitmqctl("eval 'exit(rabbit_stream_manager:lookup_leader(<<\"/\">>, <<\"" + s + "\">>),kill).'");

// give the system some time to recover
Thread.sleep(DefaultClientSubscriptions.DELAY_AFTER_METADATA_UPDATE.toMillis());
Thread.sleep(DefaultClientSubscriptions.DEFAULT_DELAY_AFTER_METADATA_UPDATE.toMillis());

Client client = cf.get();
TestUtils.waitAtMost(10, () -> {
Expand Down