/**
* Copyright 2016 Yahoo Inc.
*
* 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.yahoo.pulsar.client.api;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.bookkeeper.test.PortManager;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.yahoo.pulsar.common.api.Commands;
import com.yahoo.pulsar.common.api.proto.PulsarApi.CommandLookupTopicResponse.LookupType;
import com.yahoo.pulsar.common.api.proto.PulsarApi.ServerError;
import com.yahoo.pulsar.client.api.ClientConfiguration;
import com.yahoo.pulsar.client.api.Consumer;
import com.yahoo.pulsar.client.api.ConsumerConfiguration;
import com.yahoo.pulsar.client.api.Producer;
import com.yahoo.pulsar.client.api.PulsarClient;
import com.yahoo.pulsar.client.api.PulsarClientException;
import com.yahoo.pulsar.client.api.PulsarClientException.LookupException;
import com.yahoo.pulsar.client.api.SubscriptionType;
import com.yahoo.pulsar.client.impl.ConsumerBase;
import com.yahoo.pulsar.client.impl.ProducerBase;
import io.netty.channel.ChannelHandlerContext;
/**
*/
public class ClientErrorsTest {
MockBrokerService mockBrokerService;
private static final int WEB_SERVICE_PORT = PortManager.nextFreePort();
private static final int WEB_SERVICE_TLS_PORT = PortManager.nextFreePort();
private static final int BROKER_SERVICE_PORT = PortManager.nextFreePort();
private static final int BROKER_SERVICE_TLS_PORT = PortManager.nextFreePort();
private static int ASYNC_EVENT_COMPLETION_WAIT = 100;
private final String ASSERTION_ERROR = "AssertionError";
@BeforeClass
public void setup() {
mockBrokerService = new MockBrokerService(WEB_SERVICE_PORT, WEB_SERVICE_TLS_PORT, BROKER_SERVICE_PORT,
BROKER_SERVICE_TLS_PORT);
mockBrokerService.start();
}
@AfterClass
public void teardown() {
mockBrokerService.stop();
}
@Test
public void testMockBrokerService() throws Exception {
// test default actions of mock broker service
try {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = client.subscribe("persistent://prop/use/ns/t1", "sub1", conf);
Producer producer = client.createProducer("persistent://prop/use/ns/t1");
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
producer.send("message".getBytes());
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
consumer.unsubscribe();
producer.close();
consumer.close();
client.close();
} catch (Exception e) {
fail("None of the mocked operations should throw a client side exception");
}
}
@Test
public void testProducerCreateFailWithoutRetry() throws Exception {
producerCreateFailWithoutRetry("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedProducerCreateFailWithoutRetry() throws Exception {
producerCreateFailWithoutRetry("persistent://prop/use/ns/part-t1");
}
private void producerCreateFailWithoutRetry(String topic) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
if (counter.incrementAndGet() == 2) {
// piggyback unknown error to relay assertion failure
ctx.writeAndFlush(
Commands.newError(producer.getRequestId(), ServerError.UnknownError, ASSERTION_ERROR));
return;
}
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.AuthorizationError, "msg"));
});
try {
Producer producer = client.createProducer(topic);
} catch (Exception e) {
if (e.getMessage().equals(ASSERTION_ERROR)) {
fail("Producer create should not retry on auth error");
}
assertTrue(e instanceof PulsarClientException.AuthorizationException);
}
mockBrokerService.resetHandleProducer();
client.close();
}
@Test
public void testProducerCreateSuccessAfterRetry() throws Exception {
producerCreateSuccessAfterRetry("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedProducerCreateSuccessAfterRetry() throws Exception {
producerCreateSuccessAfterRetry("persistent://prop/use/ns/part-t1");
}
private void producerCreateSuccessAfterRetry(String topic) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
if (counter.incrementAndGet() == 2) {
ctx.writeAndFlush(Commands.newProducerSuccess(producer.getRequestId(), "default-producer"));
return;
}
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.ServiceNotReady, "msg"));
});
try {
Producer producer = client.createProducer(topic);
} catch (Exception e) {
fail("Should not fail");
}
mockBrokerService.resetHandleProducer();
client.close();
}
@Test
public void testProducerCreateFailAfterRetryTimeout() throws Exception {
producerCreateFailAfterRetryTimeout("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedProducerCreateFailAfterRetryTimeout() throws Exception {
producerCreateFailAfterRetryTimeout("persistent://prop/use/ns/part-t1");
}
private void producerCreateFailAfterRetryTimeout(String topic) throws Exception {
ClientConfiguration conf = new ClientConfiguration();
conf.setOperationTimeout(1, TimeUnit.SECONDS);
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT, conf);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
if (counter.incrementAndGet() == 2) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// do nothing
}
}
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.ServiceNotReady, "msg"));
});
try {
Producer producer = client.createProducer(topic);
fail("Should have failed");
} catch (Exception e) {
// we fail even on the retriable error
assertTrue(e instanceof PulsarClientException.LookupException);
}
mockBrokerService.resetHandleProducer();
client.close();
}
@Test
public void testProducerFailDoesNotFailOtherProducer() throws Exception {
producerFailDoesNotFailOtherProducer("persistent://prop/use/ns/t1", "persistent://prop/use/ns/t2");
}
@Test
public void testPartitionedProducerFailDoesNotFailOtherProducer() throws Exception {
producerFailDoesNotFailOtherProducer("persistent://prop/use/ns/part-t1", "persistent://prop/use/ns/part-t2");
}
private void producerFailDoesNotFailOtherProducer(String topic1, String topic2) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
if (counter.incrementAndGet() == 2) {
// fail second producer
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.AuthenticationError, "msg"));
return;
}
ctx.writeAndFlush(Commands.newProducerSuccess(producer.getRequestId(), "default-producer"));
});
ProducerBase producer1 = (ProducerBase) client.createProducer(topic1);
ProducerBase producer2 = null;
try {
producer2 = (ProducerBase) client.createProducer(topic2);
fail("Should have failed");
} catch (Exception e) {
// ok
}
assertTrue(producer1.isConnected());
assertFalse(producer2 != null && producer2.isConnected());
mockBrokerService.resetHandleProducer();
client.close();
}
@Test
public void testProducerContinuousRetryAfterSendFail() throws Exception {
producerContinuousRetryAfterSendFail("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedProducerContinuousRetryAfterSendFail() throws Exception {
producerContinuousRetryAfterSendFail("persistent://prop/use/ns/part-t1");
}
private void producerContinuousRetryAfterSendFail(String topic) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
int i = counter.incrementAndGet();
if (i == 1 || i == 5) {
// succeed on 1st and 5th attempts
ctx.writeAndFlush(Commands.newProducerSuccess(producer.getRequestId(), "default-producer"));
return;
}
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.PersistenceError, "msg"));
});
final AtomicInteger msgCounter = new AtomicInteger(0);
mockBrokerService.setHandleSend((ctx, send, headersAndPayload) -> {
// fail send once, but succeed later
if (msgCounter.incrementAndGet() == 1) {
ctx.writeAndFlush(Commands.newSendError(0, 0, new IllegalStateException("Send Failed")));
return;
}
ctx.writeAndFlush(Commands.newSendReceipt(0, 0, 1, 1));
});
try {
Producer producer = client.createProducer(topic);
producer.send("message".getBytes());
} catch (Exception e) {
fail("Should not fail");
}
mockBrokerService.resetHandleProducer();
mockBrokerService.resetHandleSend();
client.close();
}
@Test
public void testSubscribeFailWithoutRetry() throws Exception {
subscribeFailWithoutRetry("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedSubscribeFailWithoutRetry() throws Exception {
subscribeFailWithoutRetry("persistent://prop/use/ns/part-t1");
}
@Test
public void testLookupWithDisconnection() throws Exception {
final String brokerUrl = "pulsar://127.0.0.1:" + BROKER_SERVICE_PORT;
PulsarClient client = PulsarClient.create(brokerUrl);
final AtomicInteger counter = new AtomicInteger(0);
String topic = "persistent://prop/use/ns/t1";
mockBrokerService.setHandlePartitionLookup((ctx, lookup) -> {
ctx.writeAndFlush(Commands.newPartitionMetadataResponse(0, lookup.getRequestId()));
});
mockBrokerService.setHandleLookup((ctx, lookup) -> {
if (counter.incrementAndGet() == 1) {
// piggyback unknown error to relay assertion failure
ctx.close();
return;
}
ctx.writeAndFlush(
Commands.newLookupResponse(brokerUrl, null, true, LookupType.Connect, lookup.getRequestId()));
});
try {
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = client.subscribe(topic, "sub1", conf);
} catch (Exception e) {
if (e.getMessage().equals(ASSERTION_ERROR)) {
fail("Subscribe should not retry on persistence error");
}
assertTrue(e instanceof PulsarClientException.BrokerPersistenceException);
}
mockBrokerService.resetHandlePartitionLookup();
mockBrokerService.resetHandleLookup();
client.close();
}
private void subscribeFailWithoutRetry(String topic) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
if (counter.incrementAndGet() == 2) {
// piggyback unknown error to relay assertion failure
ctx.writeAndFlush(
Commands.newError(subscribe.getRequestId(), ServerError.UnknownError, ASSERTION_ERROR));
return;
}
ctx.writeAndFlush(Commands.newError(subscribe.getRequestId(), ServerError.PersistenceError, "msg"));
});
try {
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = client.subscribe(topic, "sub1", conf);
} catch (Exception e) {
if (e.getMessage().equals(ASSERTION_ERROR)) {
fail("Subscribe should not retry on persistence error");
}
assertTrue(e instanceof PulsarClientException.BrokerPersistenceException);
}
mockBrokerService.resetHandleSubscribe();
client.close();
}
@Test
public void testSubscribeSuccessAfterRetry() throws Exception {
subscribeSuccessAfterRetry("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedSubscribeSuccessAfterRetry() throws Exception {
subscribeSuccessAfterRetry("persistent://prop/use/ns/part-t1");
}
private void subscribeSuccessAfterRetry(String topic) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
if (counter.incrementAndGet() == 2) {
ctx.writeAndFlush(Commands.newSuccess(subscribe.getRequestId()));
return;
}
ctx.writeAndFlush(Commands.newError(subscribe.getRequestId(), ServerError.ServiceNotReady, "msg"));
});
try {
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = client.subscribe(topic, "sub1", conf);
} catch (Exception e) {
fail("Should not fail");
}
mockBrokerService.resetHandleSubscribe();
client.close();
}
@Test
public void testSubscribeFailAfterRetryTimeout() throws Exception {
subscribeFailAfterRetryTimeout("persistent://prop/use/ns/t1");
}
@Test
public void testPartitionedSubscribeFailAfterRetryTimeout() throws Exception {
subscribeFailAfterRetryTimeout("persistent://prop/use/ns/part-t1");
}
private void subscribeFailAfterRetryTimeout(String topic) throws Exception {
ClientConfiguration conf = new ClientConfiguration();
conf.setOperationTimeout(200, TimeUnit.MILLISECONDS);
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT, conf);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
if (counter.incrementAndGet() == 2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// do nothing
}
}
ctx.writeAndFlush(Commands.newError(subscribe.getRequestId(), ServerError.ServiceNotReady, "msg"));
});
try {
ConsumerConfiguration cConf = new ConsumerConfiguration();
cConf.setSubscriptionType(SubscriptionType.Exclusive);
client.subscribe(topic, "sub1", cConf);
fail("Should have failed");
} catch (Exception e) {
// we fail even on the retriable error
assertEquals(e.getClass(), LookupException.class);
}
mockBrokerService.resetHandleSubscribe();
client.close();
}
@Test
public void testSubscribeFailDoesNotFailOtherConsumer() throws Exception {
subscribeFailDoesNotFailOtherConsumer("persistent://prop/use/ns/t1", "persistent://prop/use/ns/t2");
}
@Test
public void testPartitionedSubscribeFailDoesNotFailOtherConsumer() throws Exception {
subscribeFailDoesNotFailOtherConsumer("persistent://prop/use/ns/part-t1", "persistent://prop/use/ns/part-t2");
}
private void subscribeFailDoesNotFailOtherConsumer(String topic1, String topic2) throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger counter = new AtomicInteger(0);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
if (counter.incrementAndGet() == 2) {
// fail second producer
ctx.writeAndFlush(Commands.newError(subscribe.getRequestId(), ServerError.AuthenticationError, "msg"));
return;
}
ctx.writeAndFlush(Commands.newSuccess(subscribe.getRequestId()));
});
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
ConsumerBase consumer1 = (ConsumerBase) client.subscribe(topic1, "sub1", conf);
ConsumerBase consumer2 = null;
try {
consumer2 = (ConsumerBase) client.subscribe(topic2, "sub1", conf);
fail("Should have failed");
} catch (Exception e) {
// ok
}
assertTrue(consumer1.isConnected());
assertFalse(consumer2 != null && consumer2.isConnected());
mockBrokerService.resetHandleSubscribe();
client.close();
}
// if a producer fails to connect while creating partitioned producer, it should close all successful connections of
// other producers and fail
@Test
public void testOneProducerFailShouldCloseAllProducersInPartitionedProducer() throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger producerCounter = new AtomicInteger(0);
final AtomicInteger closeCounter = new AtomicInteger(0);
mockBrokerService.setHandleProducer((ctx, producer) -> {
if (producerCounter.incrementAndGet() == 3) {
ctx.writeAndFlush(Commands.newError(producer.getRequestId(), ServerError.AuthorizationError, "msg"));
return;
}
ctx.writeAndFlush(Commands.newProducerSuccess(producer.getRequestId(), "default-producer"));
});
mockBrokerService.setHandleCloseProducer((ctx, closeProducer) -> {
ctx.writeAndFlush(Commands.newSuccess(closeProducer.getRequestId()));
closeCounter.incrementAndGet();
});
try {
Producer producer = client.createProducer("persistent://prop/use/ns/multi-part-t1");
fail("Should have failed with an authorization error");
} catch (Exception e) {
assertTrue(e instanceof PulsarClientException.AuthorizationException);
// should call close for 3 partitions
assertEquals(closeCounter.get(), 3);
}
mockBrokerService.resetHandleProducer();
mockBrokerService.resetHandleCloseProducer();
client.close();
}
// if a consumer fails to subscribe while creating partitioned consumer, it should close all successful connections
// of other consumers and fail
@Test
public void testOneConsumerFailShouldCloseAllConsumersInPartitionedConsumer() throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
final AtomicInteger subscribeCounter = new AtomicInteger(0);
final AtomicInteger closeCounter = new AtomicInteger(0);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
if (subscribeCounter.incrementAndGet() == 3) {
ctx.writeAndFlush(Commands.newError(subscribe.getRequestId(), ServerError.AuthenticationError, "msg"));
return;
}
ctx.writeAndFlush(Commands.newSuccess(subscribe.getRequestId()));
});
mockBrokerService.setHandleCloseConsumer((ctx, closeConsumer) -> {
ctx.writeAndFlush(Commands.newSuccess(closeConsumer.getRequestId()));
closeCounter.incrementAndGet();
});
try {
Consumer consumer = client.subscribe("persistent://prop/use/ns/multi-part-t1", "my-sub");
fail("Should have failed with an authentication error");
} catch (Exception e) {
assertTrue(e instanceof PulsarClientException.AuthenticationException);
// should call close for 3 partitions
assertEquals(closeCounter.get(), 3);
}
mockBrokerService.resetHandleSubscribe();
mockBrokerService.resetHandleCloseConsumer();
client.close();
}
@Test
public void testFlowSendWhenPartitionedSubscribeCompletes() throws Exception {
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
AtomicInteger subscribed = new AtomicInteger();
AtomicBoolean fail = new AtomicBoolean(false);
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
subscribed.incrementAndGet();
ctx.writeAndFlush(Commands.newSuccess(subscribe.getRequestId()));
});
mockBrokerService.setHandleFlow((ctx, sendFlow) -> {
if (subscribed.get() != 4) {
fail.set(true);
}
});
Consumer consumer = client.subscribe("persistent://prop/use/ns/multi-part-t1", "my-sub");
if (fail.get()) {
fail("Flow command should have been sent after all 4 partitions subscribe successfully");
}
mockBrokerService.resetHandleSubscribe();
mockBrokerService.resetHandleFlow();
client.close();
}
// Run this test multiple times to reproduce race conditions on reconnection logic
@Test(invocationCount = 10)
public void testProducerReconnect() throws Exception {
AtomicInteger numOfConnections = new AtomicInteger();
AtomicReference<ChannelHandlerContext> channelCtx = new AtomicReference<>();
AtomicBoolean msgSent = new AtomicBoolean();
mockBrokerService.setHandleConnect((ctx, connect) -> {
channelCtx.set(ctx);
ctx.writeAndFlush(Commands.newConnected(connect));
if (numOfConnections.incrementAndGet() == 2) {
// close the cnx immediately when trying to conenct the 2nd time
ctx.channel().close();
}
});
mockBrokerService.setHandleProducer((ctx, produce) -> {
ctx.writeAndFlush(Commands.newProducerSuccess(produce.getRequestId(), "default-producer"));
});
mockBrokerService.setHandleSend((ctx, sendCmd, headersAndPayload) -> {
msgSent.set(true);
ctx.writeAndFlush(Commands.newSendReceipt(0, 0, 1, 1));
});
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
Producer producer = client.createProducer("persistent://prop/use/ns/t1");
// close the cnx after creating the producer
channelCtx.get().channel().close().get();
producer.send(new byte[0]);
assertEquals(msgSent.get(), true);
assertTrue(numOfConnections.get() >= 3);
mockBrokerService.resetHandleConnect();
mockBrokerService.resetHandleProducer();
mockBrokerService.resetHandleSend();
client.close();
}
@Test
public void testConsumerReconnect() throws Exception {
AtomicInteger numOfConnections = new AtomicInteger();
AtomicReference<ChannelHandlerContext> channelCtx = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
mockBrokerService.setHandleConnect((ctx, connect) -> {
channelCtx.set(ctx);
ctx.writeAndFlush(Commands.newConnected(connect));
if (numOfConnections.incrementAndGet() == 2) {
// close the cnx immediately when trying to conenct the 2nd time
ctx.channel().close();
}
if (numOfConnections.get() == 3) {
latch.countDown();
}
});
mockBrokerService.setHandleSubscribe((ctx, subscribe) -> {
ctx.writeAndFlush(Commands.newSuccess(subscribe.getRequestId()));
});
PulsarClient client = PulsarClient.create("http://127.0.0.1:" + WEB_SERVICE_PORT);
client.subscribe("persistent://prop/use/ns/t1", "sub");
// close the cnx after creating the producer
channelCtx.get().channel().close();
latch.await(5, TimeUnit.SECONDS);
assertEquals(numOfConnections.get(), 3);
mockBrokerService.resetHandleConnect();
mockBrokerService.resetHandleSubscribe();
client.close();
}
}