/*
* 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 static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import com.google.cloud.pubsub.Publisher.Builder;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.PublishRequest;
import com.google.pubsub.v1.PublishResponse;
import com.google.pubsub.v1.PubsubMessage;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.ServerImpl;
import io.grpc.stub.StreamObserver;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.joda.time.Duration;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/** Tests for {@link PublisherImpl}. */
@RunWith(JUnit4.class)
public class PublisherImplTest {
private static final String TEST_TOPIC = "projects/test-project/topics/test-topic";
private static InProcessChannelBuilder testChannelBuilder;
@Captor private ArgumentCaptor<PublishRequest> requestCaptor;
private FakeScheduledExecutorService fakeExecutor;
private FakeCredentials testCredentials;
private static FakePublisherServiceImpl testPublisherServiceImpl;
private static ServerImpl testServer;
@BeforeClass
public static void setUpClass() throws Exception {
testPublisherServiceImpl = Mockito.spy(new FakePublisherServiceImpl());
InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName("test-server");
testChannelBuilder = InProcessChannelBuilder.forName("test-server");
InProcessChannelBuilder.forName("publisher_test");
serverBuilder.addService(testPublisherServiceImpl);
testServer = serverBuilder.build();
testServer.start();
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
testPublisherServiceImpl.reset();
Mockito.reset(testPublisherServiceImpl);
fakeExecutor = new FakeScheduledExecutorService();
testCredentials = new FakeCredentials();
}
@AfterClass
public static void tearDownClass() throws Exception {
testServer.shutdownNow().awaitTermination();
}
@Test
public void testPublishByDuration() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setMaxBatchDuration(Duration.standardSeconds(5))
// To demonstrate that reaching duration will trigger publish
.setMaxBatchMessages(10)
.build();
testPublisherServiceImpl.addPublishResponse(
PublishResponse.newBuilder().addMessageIds("1").addMessageIds("2"));
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
ListenableFuture<String> publishFuture2 = sendTestMessage(publisher, "B");
assertFalse(publishFuture1.isDone());
assertFalse(publishFuture2.isDone());
fakeExecutor.advanceTime(Duration.standardSeconds(10));
assertEquals("1", publishFuture1.get());
assertEquals("2", publishFuture2.get());
Mockito.verify(testPublisherServiceImpl)
.publish(requestCaptor.capture(), Mockito.<StreamObserver<PublishResponse>>any());
assertEquals(2, requestCaptor.getValue().getMessagesCount());
}
@Test
public void testPublishByNumBatchedMessages() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setMaxBatchDuration(Duration.standardSeconds(100))
.setMaxBatchMessages(2)
.build();
testPublisherServiceImpl
.addPublishResponse(PublishResponse.newBuilder().addMessageIds("1").addMessageIds("2"))
.addPublishResponse(PublishResponse.newBuilder().addMessageIds("3").addMessageIds("4"));
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
ListenableFuture<String> publishFuture2 = sendTestMessage(publisher, "B");
ListenableFuture<String> publishFuture3 = sendTestMessage(publisher, "C");
// Note we are not advancing time but message should still get published
assertEquals("1", publishFuture1.get());
assertEquals("2", publishFuture2.get());
assertFalse(publishFuture3.isDone());
ListenableFuture<String> publishFuture4 =
publisher.publish(PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8("D")).build());
assertEquals("3", publishFuture3.get());
assertEquals("4", publishFuture4.get());
Mockito.verify(testPublisherServiceImpl, times(2))
.publish(requestCaptor.capture(), Mockito.<StreamObserver<PublishResponse>>any());
assertEquals(2, requestCaptor.getAllValues().get(0).getMessagesCount());
assertEquals(2, requestCaptor.getAllValues().get(1).getMessagesCount());
}
@Test
public void testSinglePublishByNumBytes() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setMaxBatchDuration(Duration.standardSeconds(100))
.setMaxBatchMessages(2)
.build();
testPublisherServiceImpl
.addPublishResponse(PublishResponse.newBuilder().addMessageIds("1").addMessageIds("2"))
.addPublishResponse(PublishResponse.newBuilder().addMessageIds("3").addMessageIds("4"));
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
ListenableFuture<String> publishFuture2 = sendTestMessage(publisher, "B");
ListenableFuture<String> publishFuture3 = sendTestMessage(publisher, "C");
// Note we are not advancing time but message should still get published
assertEquals("1", publishFuture1.get());
assertEquals("2", publishFuture2.get());
assertFalse(publishFuture3.isDone());
ListenableFuture<String> publishFuture4 = sendTestMessage(publisher, "D");
assertEquals("3", publishFuture3.get());
assertEquals("4", publishFuture4.get());
Mockito.verify(testPublisherServiceImpl, times(2))
.publish(requestCaptor.capture(), Mockito.<StreamObserver<PublishResponse>>any());
}
@Test
public void testPublishMixedSizeAndDuration() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setMaxBatchDuration(Duration.standardSeconds(5))
// To demonstrate that reaching duration will trigger publish
.setMaxBatchMessages(2)
.build();
testPublisherServiceImpl.addPublishResponse(
PublishResponse.newBuilder().addMessageIds("1").addMessageIds("2"));
testPublisherServiceImpl.addPublishResponse(PublishResponse.newBuilder().addMessageIds("3"));
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
fakeExecutor.advanceTime(Duration.standardSeconds(2));
assertFalse(publishFuture1.isDone());
ListenableFuture<String> publishFuture2 = sendTestMessage(publisher, "B");
// Publishing triggered by batch size
assertEquals("1", publishFuture1.get());
assertEquals("2", publishFuture2.get());
ListenableFuture<String> publishFuture3 = sendTestMessage(publisher, "C");
assertFalse(publishFuture3.isDone());
// Publishing triggered by time
fakeExecutor.advanceTime(Duration.standardSeconds(5));
assertEquals("3", publishFuture3.get());
Mockito.verify(testPublisherServiceImpl, times(2))
.publish(requestCaptor.capture(), Mockito.<StreamObserver<PublishResponse>>any());
assertEquals(2, requestCaptor.getAllValues().get(0).getMessagesCount());
assertEquals(1, requestCaptor.getAllValues().get(1).getMessagesCount());
}
private ListenableFuture<String> sendTestMessage(Publisher publisher, String data) {
return publisher.publish(
PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8(data)).build());
}
@Test
public void testPublishFailureRetries() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setExecutor(Executors.newSingleThreadScheduledExecutor())
.setMaxBatchDuration(Duration.standardSeconds(5))
.setMaxBatchMessages(1)
.build(); // To demonstrate that reaching duration will trigger publish
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
testPublisherServiceImpl.addPublishError(new Throwable("Transiently failing"));
testPublisherServiceImpl.addPublishResponse(PublishResponse.newBuilder().addMessageIds("1"));
assertEquals("1", publishFuture1.get());
Mockito.verify(testPublisherServiceImpl, times(2))
.publish(Mockito.<PublishRequest>any(), Mockito.<StreamObserver<PublishResponse>>any());
}
@Test(expected = Throwable.class)
public void testPublishFailureRetries_exceededsRetryDuration() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setExecutor(Executors.newSingleThreadScheduledExecutor())
.setSendBatchDeadline(Duration.standardSeconds(10))
.setMaxBatchDuration(Duration.standardSeconds(5))
.setMaxBatchMessages(1)
.build(); // To demonstrate that reaching duration will trigger publish
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
// With exponential backoff, starting at 5ms we should have no more than 11 retries
for (int i = 0; i < 11; ++i) {
testPublisherServiceImpl.addPublishError(new Throwable("Transiently failing"));
}
try {
publishFuture1.get();
} finally {
Mockito.verify(testPublisherServiceImpl, atLeast(10))
.publish(Mockito.<PublishRequest>any(), Mockito.<StreamObserver<PublishResponse>>any());
}
}
@Test(expected = ExecutionException.class)
public void testPublishFailureRetries_nonRetryableFailsImmediately() throws Exception {
Publisher publisher =
getTestPublisherBuilder()
.setExecutor(Executors.newSingleThreadScheduledExecutor())
.setSendBatchDeadline(Duration.standardSeconds(10))
.setMaxBatchDuration(Duration.standardSeconds(5))
.setMaxBatchMessages(1)
.build(); // To demonstrate that reaching duration will trigger publish
ListenableFuture<String> publishFuture1 = sendTestMessage(publisher, "A");
testPublisherServiceImpl.addPublishError(new StatusException(Status.INVALID_ARGUMENT));
try {
publishFuture1.get();
} finally {
Mockito.verify(testPublisherServiceImpl)
.publish(Mockito.<PublishRequest>any(), Mockito.<StreamObserver<PublishResponse>>any());
}
}
@Test
public void testPublisherGetters() throws Exception {
FakeCredentials credentials = new FakeCredentials();
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Publisher.Builder builder = Publisher.Builder.newBuilder(TEST_TOPIC);
builder.setChannelBuilder(testChannelBuilder);
builder.setCredentials(credentials);
builder.setExecutor(executor);
builder.setFailOnFlowControlLimits(true);
builder.setMaxBatchBytes(10);
builder.setMaxBatchDuration(new Duration(11));
builder.setMaxBatchMessages(12);
builder.setMaxOutstandingBytes(13);
builder.setMaxOutstandingMessages(14);
builder.setRequestTimeout(new Duration(15));
builder.setSendBatchDeadline(new Duration(16000));
Publisher publisher = builder.build();
assertEquals(TEST_TOPIC, publisher.getTopic());
assertEquals(10, publisher.getMaxBatchBytes());
assertEquals(new Duration(11), publisher.getMaxBatchDuration());
assertEquals(12, publisher.getMaxBatchMessages());
assertEquals(Optional.of(13), publisher.getMaxOutstandingBytes());
assertEquals(Optional.of(14), publisher.getMaxOutstandingMessages());
assertTrue(publisher.failOnFlowControlLimits());
}
@Test
public void testBuilderParametersAndDefaults() {
Publisher.Builder builder = Publisher.Builder.newBuilder(TEST_TOPIC);
assertEquals(TEST_TOPIC, builder.topic);
assertEquals(Optional.absent(), builder.channelBuilder);
assertEquals(Optional.absent(), builder.executor);
assertFalse(builder.failOnFlowControlLimits);
assertEquals(Publisher.DEFAULT_MAX_BATCH_BYTES, builder.maxBatchBytes);
assertEquals(Publisher.DEFAULT_MAX_BATCH_DURATION, builder.maxBatchDuration);
assertEquals(Publisher.DEFAULT_MAX_BATCH_MESSAGES, builder.maxBatchMessages);
assertEquals(Optional.absent(), builder.maxOutstandingBytes);
assertEquals(Optional.absent(), builder.maxOutstandingMessages);
assertEquals(Publisher.DEFAULT_REQUEST_TIMEOUT, builder.requestTimeout);
assertEquals(Publisher.MIN_SEND_BATCH_DURATION, builder.sendBatchDeadline);
assertEquals(Optional.absent(), builder.userCredentials);
}
@Test
public void testBuilderInvalidArguments() {
Publisher.Builder builder = Publisher.Builder.newBuilder(TEST_TOPIC);
try {
builder.setChannelBuilder(null);
fail("Should have thrown an IllegalArgumentException");
} catch (NullPointerException expected) {
// Expected
}
try {
builder.setCredentials(null);
fail("Should have thrown an IllegalArgumentException");
} catch (NullPointerException expected) {
// Expected
}
try {
builder.setExecutor(null);
fail("Should have thrown an IllegalArgumentException");
} catch (NullPointerException expected) {
// Expected
}
try {
builder.setMaxBatchBytes(0);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
try {
builder.setMaxBatchBytes(-1);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setMaxBatchDuration(new Duration(1));
try {
builder.setMaxBatchDuration(null);
fail("Should have thrown an IllegalArgumentException");
} catch (NullPointerException expected) {
// Expected
}
try {
builder.setMaxBatchDuration(new Duration(-1));
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setMaxBatchMessages(1);
try {
builder.setMaxBatchMessages(0);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
try {
builder.setMaxBatchMessages(-1);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setMaxOutstandingBytes(1);
try {
builder.setMaxOutstandingBytes(0);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
try {
builder.setMaxOutstandingBytes(-1);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setMaxOutstandingMessages(1);
try {
builder.setMaxOutstandingMessages(0);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
try {
builder.setMaxOutstandingMessages(-1);
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setRequestTimeout(Publisher.MIN_REQUEST_TIMEOUT);
try {
builder.setRequestTimeout(Publisher.MIN_REQUEST_TIMEOUT.minus(1));
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
builder.setSendBatchDeadline(Publisher.MIN_SEND_BATCH_DURATION);
try {
builder.setSendBatchDeadline(Publisher.MIN_SEND_BATCH_DURATION.minus(1));
fail("Should have thrown an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Expected
}
}
private Builder getTestPublisherBuilder() {
return Publisher.Builder.newBuilder(TEST_TOPIC)
.setCredentials(testCredentials)
.setExecutor(fakeExecutor)
.setChannelBuilder(testChannelBuilder);
}
}