org.apache.pulsar.client.impl.TableViewImpl Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.pulsar.client.impl;
import static org.apache.pulsar.common.topics.TopicCompactionStrategy.TABLE_VIEW_TAG;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.api.CryptoKeyReader;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Reader;
import org.apache.pulsar.client.api.ReaderBuilder;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.TableView;
import org.apache.pulsar.client.api.TopicMessageId;
import org.apache.pulsar.common.naming.TopicDomain;
import org.apache.pulsar.common.topics.TopicCompactionStrategy;
@Slf4j
public class TableViewImpl implements TableView {
private final TableViewConfigurationData conf;
private final ConcurrentMap data;
private final Map immutableData;
private final CompletableFuture> reader;
private final List> listeners;
private final ReentrantLock listenersMutex;
private final boolean isPersistentTopic;
private TopicCompactionStrategy compactionStrategy;
/**
* Store the refresh tasks. When read to the position recording in the right map,
* then remove the position in the right map. If the right map is empty, complete the future in the left.
* There should be no timeout exception here, because the caller can only retry for TimeoutException.
* It will only be completed exceptionally when no more messages can be read.
*/
private final ConcurrentHashMap, Map> pendingRefreshRequests;
/**
* This map stored the read position of each partition. It is used for the following case:
*
* 1. Get last message ID.
* 2. Receive message p1-1:1, p2-1:1, p2-1:2, p3-1:1
* 3. Receive response of step1 {|p1-1:1|p2-2:2|p3-3:6|}
* 4. No more messages are written to this topic.
* As a result, the refresh operation will never be completed.
*
*/
private final ConcurrentHashMap lastReadPositions;
TableViewImpl(PulsarClientImpl client, Schema schema, TableViewConfigurationData conf) {
this.conf = conf;
this.isPersistentTopic = conf.getTopicName().startsWith(TopicDomain.persistent.toString());
this.data = new ConcurrentHashMap<>();
this.immutableData = Collections.unmodifiableMap(data);
this.listeners = new ArrayList<>();
this.listenersMutex = new ReentrantLock();
this.compactionStrategy =
TopicCompactionStrategy.load(TABLE_VIEW_TAG, conf.getTopicCompactionStrategyClassName());
this.pendingRefreshRequests = new ConcurrentHashMap<>();
this.lastReadPositions = new ConcurrentHashMap<>();
ReaderBuilder readerBuilder = client.newReader(schema)
.topic(conf.getTopicName())
.startMessageId(MessageId.earliest)
.autoUpdatePartitions(true)
.autoUpdatePartitionsInterval((int) conf.getAutoUpdatePartitionsSeconds(), TimeUnit.SECONDS)
.poolMessages(true)
.subscriptionName(conf.getSubscriptionName());
if (isPersistentTopic) {
readerBuilder.readCompacted(true);
}
CryptoKeyReader cryptoKeyReader = conf.getCryptoKeyReader();
if (cryptoKeyReader != null) {
readerBuilder.cryptoKeyReader(cryptoKeyReader);
}
readerBuilder.cryptoFailureAction(conf.getCryptoFailureAction());
this.reader = readerBuilder.createAsync();
}
CompletableFuture> start() {
return reader.thenCompose((reader) -> {
if (!isPersistentTopic) {
readTailMessages(reader);
return CompletableFuture.completedFuture(null);
}
return this.readAllExistingMessages(reader)
.thenRun(() -> readTailMessages(reader));
}).thenApply(__ -> this);
}
@Override
public int size() {
return data.size();
}
@Override
public boolean isEmpty() {
return data.isEmpty();
}
@Override
public boolean containsKey(String key) {
return data.containsKey(key);
}
@Override
public T get(String key) {
return data.get(key);
}
@Override
public Set> entrySet() {
return immutableData.entrySet();
}
@Override
public Set keySet() {
return immutableData.keySet();
}
@Override
public Collection values() {
return immutableData.values();
}
@Override
public void forEach(BiConsumer action) {
data.forEach(action);
}
@Override
public void listen(BiConsumer action) {
try {
listenersMutex.lock();
listeners.add(action);
} finally {
listenersMutex.unlock();
}
}
@Override
public void forEachAndListen(BiConsumer action) {
// Ensure we iterate over all the existing entry _and_ start the listening from the exact next message
try {
listenersMutex.lock();
// Execute the action over existing entries
forEach(action);
listeners.add(action);
} finally {
listenersMutex.unlock();
}
}
@Override
public CompletableFuture closeAsync() {
return reader.thenCompose(Reader::closeAsync);
}
@Override
public void close() throws PulsarClientException {
try {
closeAsync().get();
} catch (Exception e) {
throw PulsarClientException.unwrap(e);
}
}
private void handleMessage(Message msg) {
lastReadPositions.put(msg.getTopicName(), msg.getMessageId());
try {
if (msg.hasKey()) {
String key = msg.getKey();
T cur = msg.size() > 0 ? msg.getValue() : null;
if (log.isDebugEnabled()) {
log.debug("Applying message from topic {}. key={} value={}",
conf.getTopicName(),
key,
cur);
}
boolean update = true;
if (compactionStrategy != null) {
T prev = data.get(key);
update = !compactionStrategy.shouldKeepLeft(prev, cur);
if (!update) {
log.info("Skipped the message from topic {}. key={} value={} prev={}",
conf.getTopicName(),
key,
cur,
prev);
compactionStrategy.handleSkippedMessage(key, cur);
}
}
if (update) {
try {
listenersMutex.lock();
if (null == cur) {
data.remove(key);
} else {
data.put(key, cur);
}
for (BiConsumer listener : listeners) {
try {
listener.accept(key, cur);
} catch (Throwable t) {
log.error("Table view listener raised an exception", t);
}
}
} finally {
listenersMutex.unlock();
}
}
}
checkAllFreshTask(msg);
} finally {
msg.release();
}
}
@Override
public CompletableFuture refreshAsync() {
CompletableFuture completableFuture = new CompletableFuture<>();
reader.thenCompose(reader -> getLastMessageIds(reader).thenAccept(lastMessageIds -> {
// After get the response of lastMessageIds, put the future and result into `refreshMap`
// and then filter out partitions that has been read to the lastMessageID.
pendingRefreshRequests.put(completableFuture, lastMessageIds);
filterReceivedMessages(lastMessageIds);
// If there is no new messages, the refresh operation could be completed right now.
if (lastMessageIds.isEmpty()) {
pendingRefreshRequests.remove(completableFuture);
completableFuture.complete(null);
}
})).exceptionally(throwable -> {
completableFuture.completeExceptionally(throwable);
pendingRefreshRequests.remove(completableFuture);
return null;
});
return completableFuture;
}
@Override
public void refresh() throws PulsarClientException {
try {
refreshAsync().get();
} catch (Exception e) {
throw PulsarClientException.unwrap(e);
}
}
private CompletableFuture readAllExistingMessages(Reader reader) {
long startTime = System.nanoTime();
AtomicLong messagesRead = new AtomicLong();
CompletableFuture future = new CompletableFuture<>();
getLastMessageIds(reader).thenAccept(maxMessageIds -> {
readAllExistingMessages(reader, future, startTime, messagesRead, maxMessageIds);
}).exceptionally(ex -> {
future.completeExceptionally(ex);
return null;
});
return future;
}
private CompletableFuture