/*
* Copyright 2016 Google 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
*
* http://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.google.cloud.pubsub;
import static org.junit.Assert.assertEquals;
import com.google.cloud.pubsub.FakeSubscriberServiceImpl.ModifyAckDeadline;
import com.google.cloud.pubsub.Subscriber.Builder;
import com.google.cloud.pubsub.Subscriber.MessageReceiver;
import com.google.cloud.pubsub.Subscriber.MessageReceiver.AckReply;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Service.State;
import com.google.common.util.concurrent.SettableFuture;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.PullResponse;
import com.google.pubsub.v1.ReceivedMessage;
import com.google.pubsub.v1.StreamingPullResponse;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.ServerImpl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.joda.time.Duration;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/** Tests for {@link SubscriberImpl}. */
@RunWith(Parameterized.class)
public class SubscriberImplTest {
private static final String TEST_SUBSCRIPTION =
"projects/test-project/subscriptions/test-subscription";
private static final PubsubMessage TEST_MESSAGE =
PubsubMessage.newBuilder().setMessageId("1").build();
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{true}, {false}});
}
private final boolean isStreamingTest;
private InProcessChannelBuilder testChannelBuilder;
private FakeScheduledExecutorService fakeExecutor;
private FakeSubscriberServiceImpl fakeSubscriberServiceImpl;
private ServerImpl testServer;
private FakeCredentials testCredentials;
private TestReceiver testReceiver;
static class TestReceiver implements MessageReceiver {
private final Deque<SettableFuture<AckReply>> outstandingMessageReplies =
new ConcurrentLinkedDeque<>();
private AckReply ackReply = AckReply.ACK;
private Optional<CountDownLatch> messageCountLatch = Optional.absent();
private Optional<Throwable> error = Optional.absent();
private boolean explicitAckReplies;
void setReply(AckReply ackReply) {
this.ackReply = ackReply;
}
void setErrorReply(Throwable error) {
this.error = Optional.of(error);
}
void setExplicitAck(boolean explicitAckReplies) {
this.explicitAckReplies = explicitAckReplies;
}
void setExpectedMessages(int expected) {
this.messageCountLatch = Optional.of(new CountDownLatch(expected));
}
void waitForExpectedMessages() throws InterruptedException {
if (messageCountLatch.isPresent()) {
messageCountLatch.get().await();
}
}
@Override
public ListenableFuture<AckReply> receiveMessage(PubsubMessage message) {
if (messageCountLatch.isPresent()) {
messageCountLatch.get().countDown();
}
SettableFuture<AckReply> reply = SettableFuture.create();
if (explicitAckReplies) {
outstandingMessageReplies.add(reply);
} else {
if (error.isPresent()) {
reply.setException(error.get());
} else {
reply.set(ackReply);
}
}
return reply;
}
public void replyNextOutstandingMessage() {
Preconditions.checkState(explicitAckReplies);
SettableFuture<AckReply> reply = outstandingMessageReplies.poll();
if (error.isPresent()) {
reply.setException(error.get());
} else {
reply.set(ackReply);
}
}
public void replyAllOutstandingMessage() {
Preconditions.checkState(explicitAckReplies);
while (!outstandingMessageReplies.isEmpty()) {
replyNextOutstandingMessage();
}
}
}
public SubscriberImplTest(boolean streamingTest) {
this.isStreamingTest = streamingTest;
}
@Rule public TestName testName = new TestName();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName(testName.getMethodName());
fakeSubscriberServiceImpl = Mockito.spy(new FakeSubscriberServiceImpl());
fakeExecutor = new FakeScheduledExecutorService();
testChannelBuilder = InProcessChannelBuilder.forName(testName.getMethodName());
serverBuilder.addService(fakeSubscriberServiceImpl);
testServer = serverBuilder.build();
testServer.start();
testReceiver = new TestReceiver();
testCredentials = new FakeCredentials();
}
@After
public void tearDown() throws Exception {
testServer.shutdownNow().awaitTermination(10, TimeUnit.SECONDS);
fakeSubscriberServiceImpl.reset();
}
@Test
public void testAckSingleMessage() throws Exception {
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
List<String> testAckIds = ImmutableList.of("A");
sendMessages(testAckIds);
// Trigger ack sending
subscriber.stopAsync().awaitTerminated();
assertEquivalent(testAckIds, fakeSubscriberServiceImpl.waitAndConsumeReceivedAcks(1));
}
@Test
public void testNackSingleMessage() throws Exception {
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
testReceiver.setReply(AckReply.NACK);
sendMessages(ImmutableList.of("A"));
// Trigger ack sending
subscriber.stopAsync().awaitTerminated();
assertEquivalent(
ImmutableList.of(new ModifyAckDeadline("A", 0)),
fakeSubscriberServiceImpl.waitAndConsumeModifyAckDeadlines(1));
}
@Test
public void testReceiverError_NacksMessage() throws Exception {
testReceiver.setErrorReply(new Exception("Can't process message"));
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
sendMessages(ImmutableList.of("A"));
// Trigger nack sending
subscriber.stopAsync().awaitTerminated();
// TODO: it's really unstable test and cause of most travis failures. To solve it I decided to use sleep at the time. But we should find a real reason of it or modify requirements.
Thread.sleep(200L);
assertEquivalent(
ImmutableList.of(new ModifyAckDeadline("A", 0)),
fakeSubscriberServiceImpl.getModifyAckDeadlines());
}
@Test
public void testBatchAcks() throws Exception {
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
List<String> testAckIdsBatch1 = ImmutableList.of("A", "B", "C");
sendMessages(testAckIdsBatch1);
// Trigger ack sending
fakeExecutor.advanceTime(StreamingSubscriberConnection.PENDING_ACKS_SEND_DELAY);
assertEquivalent(testAckIdsBatch1, fakeSubscriberServiceImpl.waitAndConsumeReceivedAcks(3));
// Ensures the next ack sending alarm gets properly setup
List<String> testAckIdsBatch2 = ImmutableList.of("D", "E");
sendMessages(testAckIdsBatch2);
fakeExecutor.advanceTime(StreamingSubscriberConnection.PENDING_ACKS_SEND_DELAY);
assertEquivalent(testAckIdsBatch2, fakeSubscriberServiceImpl.waitAndConsumeReceivedAcks(2));
subscriber.stopAsync().awaitTerminated();
}
@Test
public void testBatchAcksAndNacks() throws Exception {
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
// Send messages to be acked
List<String> testAckIdsBatch1 = ImmutableList.of("A", "B", "C");
sendMessages(testAckIdsBatch1);
// Send messages to be nacked
List<String> testAckIdsBatch2 = ImmutableList.of("D", "E");
// Nack messages
testReceiver.setReply(AckReply.NACK);
sendMessages(testAckIdsBatch2);
// Trigger ack sending
fakeExecutor.advanceTime(StreamingSubscriberConnection.PENDING_ACKS_SEND_DELAY);
assertEquivalent(testAckIdsBatch1, fakeSubscriberServiceImpl.waitAndConsumeReceivedAcks(3));
assertEquivalent(
ImmutableList.of(new ModifyAckDeadline("D", 0), new ModifyAckDeadline("E", 0)),
fakeSubscriberServiceImpl.waitAndConsumeModifyAckDeadlines(2));
subscriber.stopAsync().awaitTerminated();
}
@Test
public void testModifyAckDeadline() throws Exception {
Subscriber subscriber =
startSubscriber(
getTestSubscriberBuilder(testReceiver)
.setAckExpirationPadding(Duration.standardSeconds(1)));
// Send messages to be acked
List<String> testAckIdsBatch = ImmutableList.of("A", "B", "C");
testReceiver.setExplicitAck(true);
sendMessages(testAckIdsBatch);
// Trigger modify ack deadline sending - 10s initial stream ack deadline - 1 padding
fakeExecutor.advanceTime(Duration.standardSeconds(9));
assertEquivalentWithTransformation(
testAckIdsBatch,
fakeSubscriberServiceImpl.waitAndConsumeModifyAckDeadlines(3),
new Function<String, ModifyAckDeadline>() {
@Override
public ModifyAckDeadline apply(String ack) {
return new ModifyAckDeadline(ack, 2); // 2 seconds is the initial mod ack deadline
}
});
// Trigger modify ack deadline sending - 2s of the renewed deadlines
fakeExecutor.advanceTime(Duration.standardSeconds(2));
assertEquivalentWithTransformation(
testAckIdsBatch,
fakeSubscriberServiceImpl.waitAndConsumeModifyAckDeadlines(3),
new Function<String, ModifyAckDeadline>() {
@Override
public ModifyAckDeadline apply(String ack) {
return new ModifyAckDeadline(ack, 4);
}
});
testReceiver.replyAllOutstandingMessage();
subscriber.stopAsync().awaitTerminated();
}
@Test
public void testStreamAckDeadlineUpdate() throws Exception {
if (!isStreamingTest) {
// This test is not applicable to polling.
return;
}
Subscriber subscriber =
startSubscriber(
getTestSubscriberBuilder(testReceiver)
.setAckExpirationPadding(Duration.standardSeconds(1)));
fakeSubscriberServiceImpl.waitForStreamAckDeadline(10);
// Send messages to be acked
testReceiver.setExplicitAck(true);
sendMessages(ImmutableList.of("A"));
// Make the ack latency of the receiver equals 20 seconds
fakeExecutor.advanceTime(Duration.standardSeconds(20));
testReceiver.replyNextOutstandingMessage();
// Wait for an ack deadline update
fakeExecutor.advanceTime(Duration.standardSeconds(60));
fakeSubscriberServiceImpl.waitForStreamAckDeadline(20);
// Send more messages to be acked
testReceiver.setExplicitAck(true);
for (int i = 0; i < 999; i++) {
sendMessages(ImmutableList.of(Integer.toString(i)));
}
// Reduce the 99th% ack latency of the receiver to 10 seconds
fakeExecutor.advanceTime(Duration.standardSeconds(10));
for (int i = 0; i < 999; i++) {
testReceiver.replyNextOutstandingMessage();
}
// Wait for an ack deadline update
fakeExecutor.advanceTime(Duration.standardSeconds(60));
fakeSubscriberServiceImpl.waitForStreamAckDeadline(10);
subscriber.stopAsync().awaitTerminated();
}
@Test
public void testOpenedChannels() throws Exception {
if (!isStreamingTest) {
// This test is not applicable to polling.
return;
}
final int expectedChannelCount =
Runtime.getRuntime().availableProcessors() * SubscriberImpl.CHANNELS_PER_CORE;
Subscriber subscriber = startSubscriber(getTestSubscriberBuilder(testReceiver));
assertEquals(
expectedChannelCount, fakeSubscriberServiceImpl.waitForOpenedStreams(expectedChannelCount));
subscriber.stopAsync().awaitTerminated();
}
@Test
public void testFailedChannel_recoverableError_channelReopened() throws Exception {
if (!isStreamingTest) {
// This test is not applicable to polling.
return;
}
final int expectedChannelCount =
Runtime.getRuntime().availableProcessors() * SubscriberImpl.CHANNELS_PER_CORE;
Subscriber subscriber =
startSubscriber(
getTestSubscriberBuilder(testReceiver)
.setExecutor(Executors.newSingleThreadScheduledExecutor()));
// Recoverable error
fakeSubscriberServiceImpl.sendError(new StatusException(Status.INTERNAL));
assertEquals(1, fakeSubscriberServiceImpl.waitForClosedStreams(1));
assertEquals(
expectedChannelCount, fakeSubscriberServiceImpl.waitForOpenedStreams(expectedChannelCount));
subscriber.stopAsync().awaitTerminated();
}
@Test(expected = IllegalStateException.class)
public void testFailedChannel_fatalError_subscriberFails() throws Exception {
if (!isStreamingTest) {
// This test is not applicable to polling.
throw new IllegalStateException("To fullfil test expectation");
}
Subscriber subscriber =
startSubscriber(
getTestSubscriberBuilder(testReceiver)
.setExecutor(Executors.newScheduledThreadPool(10)));
// Fatal error
fakeSubscriberServiceImpl.sendError(new StatusException(Status.INVALID_ARGUMENT));
try {
subscriber.awaitTerminated();
} finally {
// The subscriber must finish with an state error because its FAILED status.
assertEquals(State.FAILED, subscriber.state());
assertEquals(
Status.INVALID_ARGUMENT,
((StatusRuntimeException) subscriber.failureCause()).getStatus());
}
}
private Subscriber startSubscriber(Builder testSubscriberBuilder) throws Exception {
Subscriber subscriber = testSubscriberBuilder.build();
subscriber.startAsync().awaitRunning();
if (!isStreamingTest) {
// Shutdown streaming
fakeSubscriberServiceImpl.sendError(new StatusException(Status.UNIMPLEMENTED));
}
return subscriber;
}
private void sendMessages(Iterable<String> ackIds) throws InterruptedException {
List<ReceivedMessage> messages = new ArrayList<ReceivedMessage>();
for (String ackId : ackIds) {
messages.add(ReceivedMessage.newBuilder().setAckId(ackId).setMessage(TEST_MESSAGE).build());
}
testReceiver.setExpectedMessages(messages.size());
if (isStreamingTest) {
fakeSubscriberServiceImpl.sendStreamingResponse(
StreamingPullResponse.newBuilder().addAllReceivedMessages(messages).build());
} else {
fakeSubscriberServiceImpl.enqueuePullResponse(
PullResponse.newBuilder().addAllReceivedMessages(messages).build());
}
testReceiver.waitForExpectedMessages();
}
private Builder getTestSubscriberBuilder(MessageReceiver receiver) {
return Subscriber.Builder.newBuilder(TEST_SUBSCRIPTION, receiver)
.setExecutor(fakeExecutor)
.setCredentials(testCredentials)
.setChannelBuilder(testChannelBuilder);
}
@SuppressWarnings("unchecked")
private <T> void assertEquivalent(Collection<T> expectedElems, Collection<T> target) {
List<T> remaining = new ArrayList<T>(target.size());
remaining.addAll(target);
for (T expectedElem : expectedElems) {
if (!remaining.contains(expectedElem)) {
throw new AssertionError(
String.format("Expected element %s is not contained in %s", expectedElem, target));
}
remaining.remove(expectedElem);
}
}
@SuppressWarnings("unchecked")
private <T, E> void assertEquivalentWithTransformation(
Collection<E> expectedElems, Collection<T> target, Function<E, T> transform) {
List<T> remaining = new ArrayList<T>(target.size());
remaining.addAll(target);
for (E expectedElem : expectedElems) {
if (!remaining.contains(transform.apply(expectedElem))) {
throw new AssertionError(
String.format("Expected element %s is not contained in %s", expectedElem, target));
}
remaining.remove(expectedElem);
}
}
}