/** * 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.impl; import java.util.IdentityHashMap; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; 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.MessageId; import com.yahoo.pulsar.client.api.Producer; import com.yahoo.pulsar.client.api.ProducerConfiguration; import com.yahoo.pulsar.client.api.PulsarClient; import com.yahoo.pulsar.client.api.PulsarClientException; import com.yahoo.pulsar.client.api.Reader; import com.yahoo.pulsar.client.api.ReaderConfiguration; import com.yahoo.pulsar.client.util.ExecutorProvider; import com.yahoo.pulsar.client.util.FutureUtil; import com.yahoo.pulsar.common.naming.DestinationName; import com.yahoo.pulsar.common.partition.PartitionedTopicMetadata; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.HashedWheelTimer; import io.netty.util.Timer; import io.netty.util.concurrent.DefaultThreadFactory; public class PulsarClientImpl implements PulsarClient { private static final Logger log = LoggerFactory.getLogger(PulsarClientImpl.class); private final ClientConfiguration conf; private HttpClient httpClient; private final LookupService lookup; private final ConnectionPool cnxPool; private final Timer timer; private final ExecutorProvider externalExecutorProvider; enum State { Open, Closing, Closed } private AtomicReference<State> state = new AtomicReference<>(); private final IdentityHashMap<ProducerBase, Boolean> producers; private final IdentityHashMap<ConsumerBase, Boolean> consumers; private final AtomicLong producerIdGenerator = new AtomicLong(); private final AtomicLong consumerIdGenerator = new AtomicLong(); private final AtomicLong requestIdGenerator = new AtomicLong(); private final EventLoopGroup eventLoopGroup; public PulsarClientImpl(String serviceUrl, ClientConfiguration conf) throws PulsarClientException { this(serviceUrl, conf, getEventLoopGroup(conf)); } public PulsarClientImpl(String serviceUrl, ClientConfiguration conf, EventLoopGroup eventLoopGroup) throws PulsarClientException { if (serviceUrl == null || conf == null || eventLoopGroup == null) { throw new PulsarClientException.InvalidConfigurationException("Invalid client configuration"); } this.eventLoopGroup = eventLoopGroup; this.conf = conf; conf.getAuthentication().start(); cnxPool = new ConnectionPool(this, eventLoopGroup); if (serviceUrl.startsWith("http")) { httpClient = new HttpClient(serviceUrl, conf.getAuthentication(), eventLoopGroup, conf.isTlsAllowInsecureConnection(), conf.getTlsTrustCertsFilePath()); lookup = new HttpLookupService(httpClient, conf.isUseTls()); } else { lookup = new BinaryProtoLookupService(this, serviceUrl, conf.isUseTls()); } timer = new HashedWheelTimer(new DefaultThreadFactory("pulsar-timer"), 1, TimeUnit.MILLISECONDS); externalExecutorProvider = new ExecutorProvider(conf.getListenerThreads(), "pulsar-external-listener"); producers = Maps.newIdentityHashMap(); consumers = Maps.newIdentityHashMap(); state.set(State.Open); } public ClientConfiguration getConfiguration() { return conf; } @Override public Producer createProducer(String destination) throws PulsarClientException { try { return createProducerAsync(destination, new ProducerConfiguration()).get(); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof PulsarClientException) { throw (PulsarClientException) t; } else { throw new PulsarClientException(t); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new PulsarClientException(e); } } @Override public Producer createProducer(final String destination, final ProducerConfiguration conf) throws PulsarClientException { try { return createProducerAsync(destination, conf).get(); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof PulsarClientException) { throw (PulsarClientException) t; } else { throw new PulsarClientException(t); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new PulsarClientException(e); } } @Override public CompletableFuture<Producer> createProducerAsync(String topic) { return createProducerAsync(topic, new ProducerConfiguration()); } @Override public CompletableFuture<Producer> createProducerAsync(final String topic, final ProducerConfiguration conf) { return createProducerAsync(topic, conf, null); } public CompletableFuture<Producer> createProducerAsync(final String topic, final ProducerConfiguration conf, String producerName) { if (state.get() != State.Open) { return FutureUtil.failedFuture(new PulsarClientException.AlreadyClosedException("Client already closed")); } if (!DestinationName.isValid(topic)) { return FutureUtil.failedFuture(new PulsarClientException.InvalidTopicNameException("Invalid topic name")); } if (conf == null) { return FutureUtil.failedFuture( new PulsarClientException.InvalidConfigurationException("Producer configuration undefined")); } CompletableFuture<Producer> producerCreatedFuture = new CompletableFuture<>(); getPartitionedTopicMetadata(topic).thenAccept(metadata -> { if (log.isDebugEnabled()) { log.debug("[{}] Received topic metadata. partitions: {}", topic, metadata.partitions); } ProducerBase producer; if (metadata.partitions > 1) { producer = new PartitionedProducerImpl(PulsarClientImpl.this, topic, conf, metadata.partitions, producerCreatedFuture); } else { producer = new ProducerImpl(PulsarClientImpl.this, topic, producerName, conf, producerCreatedFuture, -1); } synchronized (producers) { producers.put(producer, Boolean.TRUE); } }).exceptionally(ex -> { log.warn("[{}] Failed to get partitioned topic metadata: {}", topic, ex.getMessage()); producerCreatedFuture.completeExceptionally(ex); return null; }); return producerCreatedFuture; } @Override public Consumer subscribe(final String topic, final String subscription) throws PulsarClientException { return subscribe(topic, subscription, new ConsumerConfiguration()); } @Override public Consumer subscribe(String topic, String subscription, ConsumerConfiguration conf) throws PulsarClientException { try { return subscribeAsync(topic, subscription, conf).get(); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof PulsarClientException) { throw (PulsarClientException) t; } else { throw new PulsarClientException(t); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new PulsarClientException(e); } } @Override public CompletableFuture<Consumer> subscribeAsync(String topic, String subscription) { return subscribeAsync(topic, subscription, new ConsumerConfiguration()); } @Override public CompletableFuture<Consumer> subscribeAsync(final String topic, final String subscription, final ConsumerConfiguration conf) { if (state.get() != State.Open) { return FutureUtil.failedFuture(new PulsarClientException.AlreadyClosedException("Client already closed")); } if (!DestinationName.isValid(topic)) { return FutureUtil.failedFuture(new PulsarClientException.InvalidTopicNameException("Invalid topic name")); } if (subscription == null) { return FutureUtil .failedFuture(new PulsarClientException.InvalidConfigurationException("Invalid subscription name")); } if (conf == null) { return FutureUtil.failedFuture( new PulsarClientException.InvalidConfigurationException("Consumer configuration undefined")); } CompletableFuture<Consumer> consumerSubscribedFuture = new CompletableFuture<>(); getPartitionedTopicMetadata(topic).thenAccept(metadata -> { if (log.isDebugEnabled()) { log.debug("[{}] Received topic metadata. partitions: {}", topic, metadata.partitions); } ConsumerBase consumer; // gets the next single threaded executor from the list of executors ExecutorService listenerThread = externalExecutorProvider.getExecutor(); if (metadata.partitions > 1) { consumer = new PartitionedConsumerImpl(PulsarClientImpl.this, topic, subscription, conf, metadata.partitions, listenerThread, consumerSubscribedFuture); } else { consumer = new ConsumerImpl(PulsarClientImpl.this, topic, subscription, conf, listenerThread, -1, consumerSubscribedFuture); } synchronized (consumers) { consumers.put(consumer, Boolean.TRUE); } }).exceptionally(ex -> { log.warn("[{}] Failed to get partitioned topic metadata", topic, ex); consumerSubscribedFuture.completeExceptionally(ex); return null; }); return consumerSubscribedFuture; } @Override public Reader createReader(String topic, MessageId startMessageId, ReaderConfiguration conf) throws PulsarClientException { try { return createReaderAsync(topic, startMessageId, conf).get(); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof PulsarClientException) { throw (PulsarClientException) t; } else { throw new PulsarClientException(t); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new PulsarClientException(e); } } @Override public CompletableFuture<Reader> createReaderAsync(String topic, MessageId startMessageId, ReaderConfiguration conf) { if (state.get() != State.Open) { return FutureUtil.failedFuture(new PulsarClientException.AlreadyClosedException("Client already closed")); } if (!DestinationName.isValid(topic)) { return FutureUtil.failedFuture(new PulsarClientException.InvalidTopicNameException("Invalid topic name")); } if (startMessageId == null) { return FutureUtil .failedFuture(new PulsarClientException.InvalidConfigurationException("Invalid startMessageId")); } if (conf == null) { return FutureUtil.failedFuture( new PulsarClientException.InvalidConfigurationException("Consumer configuration undefined")); } CompletableFuture<Reader> readerFuture = new CompletableFuture<>(); getPartitionedTopicMetadata(topic).thenAccept(metadata -> { if (log.isDebugEnabled()) { log.debug("[{}] Received topic metadata. partitions: {}", topic, metadata.partitions); } if (metadata.partitions > 1) { readerFuture.completeExceptionally( new PulsarClientException("Topic reader cannot be created on a partitioned topic")); return; } CompletableFuture<Consumer> consumerSubscribedFuture = new CompletableFuture<>(); // gets the next single threaded executor from the list of executors ExecutorService listenerThread = externalExecutorProvider.getExecutor(); ReaderImpl reader = new ReaderImpl(PulsarClientImpl.this, topic, startMessageId, conf, listenerThread, consumerSubscribedFuture); synchronized (consumers) { consumers.put(reader.getConsumer(), Boolean.TRUE); } consumerSubscribedFuture.thenRun(() -> { readerFuture.complete(reader); }).exceptionally(ex -> { log.warn("[{}] Failed to get create topic reader", topic, ex); readerFuture.completeExceptionally(ex); return null; }); }).exceptionally(ex -> { log.warn("[{}] Failed to get partitioned topic metadata", topic, ex); readerFuture.completeExceptionally(ex); return null; }); return readerFuture; } @Override public void close() throws PulsarClientException { try { closeAsync().get(); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof PulsarClientException) { throw (PulsarClientException) t; } else { throw new PulsarClientException(t); } } catch (InterruptedException e) { throw new PulsarClientException(e); } } @Override public CompletableFuture<Void> closeAsync() { log.info("Client closing. URL: {}", lookup.getServiceUrl()); if (!state.compareAndSet(State.Open, State.Closing)) { return FutureUtil.failedFuture(new PulsarClientException.AlreadyClosedException("Client already closed")); } final CompletableFuture<Void> closeFuture = new CompletableFuture<>(); List<CompletableFuture<Void>> futures = Lists.newArrayList(); synchronized (producers) { // Copy to a new list, because the closing will trigger a removal from the map // and invalidate the iterator List<ProducerBase> producersToClose = Lists.newArrayList(producers.keySet()); producersToClose.forEach(p -> futures.add(p.closeAsync())); } synchronized (consumers) { List<ConsumerBase> consumersToClose = Lists.newArrayList(consumers.keySet()); consumersToClose.forEach(c -> futures.add(c.closeAsync())); } FutureUtil.waitForAll(futures).thenRun(() -> { // All producers & consumers are now closed, we can stop the client safely try { shutdown(); closeFuture.complete(null); state.set(State.Closed); } catch (PulsarClientException e) { closeFuture.completeExceptionally(e); } }).exceptionally(exception -> { closeFuture.completeExceptionally(exception); return null; }); return closeFuture; } @Override public void shutdown() throws PulsarClientException { try { if (httpClient != null) { httpClient.close(); } cnxPool.close(); timer.stop(); externalExecutorProvider.shutdownNow(); conf.getAuthentication().close(); } catch (Throwable t) { log.warn("Failed to shutdown Pulsar client", t); throw new PulsarClientException(t); } } protected CompletableFuture<ClientCnx> getConnection(final String topic) { DestinationName destinationName = DestinationName.get(topic); return lookup.getBroker(destinationName).thenCompose(cnxPool::getConnection); } protected Timer timer() { return timer; } ExecutorProvider externalExecutorProvider() { return externalExecutorProvider; } long newProducerId() { return producerIdGenerator.getAndIncrement(); } long newConsumerId() { return consumerIdGenerator.getAndIncrement(); } long newRequestId() { return requestIdGenerator.getAndIncrement(); } ConnectionPool getCnxPool() { return cnxPool; } EventLoopGroup eventLoopGroup() { return eventLoopGroup; } private CompletableFuture<PartitionedTopicMetadata> getPartitionedTopicMetadata(String topic) { CompletableFuture<PartitionedTopicMetadata> metadataFuture; try { DestinationName destinationName = DestinationName.get(topic); metadataFuture = lookup.getPartitionedTopicMetadata(destinationName); } catch (IllegalArgumentException e) { return FutureUtil.failedFuture(e); } return metadataFuture; } private static EventLoopGroup getEventLoopGroup(ClientConfiguration conf) { int numThreads = conf.getIoThreads(); ThreadFactory threadFactory = new DefaultThreadFactory("pulsar-client-io"); if (SystemUtils.IS_OS_LINUX) { try { return new EpollEventLoopGroup(numThreads, threadFactory); } catch (ExceptionInInitializerError | NoClassDefFoundError | UnsatisfiedLinkError e) { if (log.isDebugEnabled()) { log.debug("Unable to load EpollEventLoop", e); } return new NioEventLoopGroup(numThreads, threadFactory); } } else { return new NioEventLoopGroup(numThreads, threadFactory); } } void cleanupProducer(ProducerBase producer) { synchronized (producers) { producers.remove(producer); } } void cleanupConsumer(ConsumerBase consumer) { synchronized (consumers) { consumers.remove(consumer); } } }