-
Notifications
You must be signed in to change notification settings - Fork 3.7k
PIP 68: Exclusive Producer #8685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a60156f
3524cc0
1f86250
69a0a8f
9c0dda2
eba7bbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,22 +18,30 @@ | |
| */ | ||
| package org.apache.pulsar.broker.service; | ||
|
|
||
| import static com.google.common.base.Preconditions.checkArgument; | ||
| import static org.apache.bookkeeper.mledger.impl.ManagedLedgerMBeanImpl.ENTRY_LATENCY_BUCKETS_USEC; | ||
| import static org.apache.pulsar.broker.cache.ConfigurationCacheService.POLICIES; | ||
| import com.google.common.base.MoreObjects; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicLongFieldUpdater; | ||
| import java.util.concurrent.atomic.LongAdder; | ||
| import java.util.concurrent.locks.ReentrantLock; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
|
|
||
| import org.apache.bookkeeper.mledger.util.StatsBuckets; | ||
| import org.apache.pulsar.broker.ServiceConfiguration; | ||
| import org.apache.pulsar.broker.admin.AdminResource; | ||
| import org.apache.pulsar.broker.service.BrokerServiceException.ConsumerBusyException; | ||
| import org.apache.pulsar.broker.service.BrokerServiceException.ProducerBusyException; | ||
| import org.apache.pulsar.broker.service.BrokerServiceException.ProducerFencedException; | ||
| import org.apache.pulsar.broker.service.BrokerServiceException.TopicTerminatedException; | ||
| import org.apache.pulsar.broker.service.schema.SchemaRegistryService; | ||
| import org.apache.pulsar.broker.service.schema.exceptions.IncompatibleSchemaException; | ||
| import org.apache.pulsar.broker.stats.prometheus.metrics.Summary; | ||
|
|
@@ -101,6 +109,13 @@ public abstract class AbstractTopic implements Topic { | |
| protected CompletableFuture<TransactionBuffer> transactionBuffer; | ||
| protected ReentrantLock transactionBufferLock = new ReentrantLock(); | ||
|
|
||
| protected volatile Optional<Long> topicEpoch = Optional.empty(); | ||
| private volatile boolean hasExclusiveProducer; | ||
|
|
||
| private static final AtomicLongFieldUpdater<AbstractTopic> USAGE_COUNT_UPDATER = | ||
| AtomicLongFieldUpdater.newUpdater(AbstractTopic.class, "usageCount"); | ||
| private volatile long usageCount = 0; | ||
|
|
||
| public AbstractTopic(String topic, BrokerService brokerService) { | ||
| this.topic = topic; | ||
| this.brokerService = brokerService; | ||
|
|
@@ -316,6 +331,106 @@ public CompletableFuture<Void> checkSchemaCompatibleForConsumer(SchemaData schem | |
| .checkConsumerCompatibility(id, schema, schemaCompatibilityStrategy); | ||
| } | ||
|
|
||
| @Override | ||
| public CompletableFuture<Optional<Long>> addProducer(Producer producer) { | ||
| checkArgument(producer.getTopic() == this); | ||
|
|
||
| CompletableFuture<Optional<Long>> future = new CompletableFuture<>(); | ||
|
|
||
| incrementTopicEpochIfNeeded(producer) | ||
| .thenAccept(epoch -> { | ||
| lock.readLock().lock(); | ||
| try { | ||
| brokerService.checkTopicNsOwnership(getName()); | ||
| checkTopicFenced(); | ||
| if (isTerminated()) { | ||
| log.warn("[{}] Attempting to add producer to a terminated topic", topic); | ||
| throw new TopicTerminatedException("Topic was already terminated"); | ||
| } | ||
| internalAddProducer(producer); | ||
|
|
||
| USAGE_COUNT_UPDATER.incrementAndGet(this); | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[{}] [{}] Added producer -- count: {}", topic, producer.getProducerName(), | ||
| USAGE_COUNT_UPDATER.get(this)); | ||
| } | ||
|
|
||
| future.complete(epoch); | ||
| } catch (Throwable e) { | ||
| future.completeExceptionally(e); | ||
| } finally { | ||
| lock.readLock().unlock(); | ||
| } | ||
| }).exceptionally(ex -> { | ||
| future.completeExceptionally(ex); | ||
| return null; | ||
| }); | ||
|
|
||
| return future; | ||
| } | ||
|
|
||
| protected CompletableFuture<Optional<Long>> incrementTopicEpochIfNeeded(Producer producer) { | ||
| lock.writeLock().lock(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we avoid locking for normal producer usecase?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to @rdhabalia suggestion
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that we can avoid the locking, and I'm not sure that it's something to worry about in the context of adding a producer. |
||
| try { | ||
| switch (producer.getAccessMode()) { | ||
| case Shared: | ||
| if (hasExclusiveProducer) { | ||
| return FutureUtil.failedFuture(new ProducerBusyException( | ||
| "Topic has an existing exclusive producer: " + producers.keys().nextElement())); | ||
| } else { | ||
| // Normal producer getting added, we don't need a new epoch | ||
| return CompletableFuture.completedFuture(topicEpoch); | ||
| } | ||
|
|
||
| case Exclusive: | ||
| if (hasExclusiveProducer) { | ||
| return FutureUtil.failedFuture(new ProducerFencedException( | ||
| "Topic has an existing exclusive producer: " + producers.keys().nextElement())); | ||
| } else if (!producers.isEmpty()) { | ||
| return FutureUtil.failedFuture(new ProducerFencedException("Topic has existing shared producers")); | ||
| } else if (producer.getTopicEpoch().isPresent() | ||
| && producer.getTopicEpoch().get() < topicEpoch.orElse(-1L)) { | ||
| // If a producer reconnects, but all the topic epoch has already moved forward, this producer needs | ||
| // to be fenced, because a new producer had been present in between. | ||
| return FutureUtil.failedFuture(new ProducerFencedException( | ||
| String.format("Topic epoch has already moved. Current epoch: %d, Producer epoch: %d", | ||
| topicEpoch.get(), producer.getTopicEpoch().get()))); | ||
| } else { | ||
| // There are currently no existing producers | ||
| hasExclusiveProducer = true; | ||
|
|
||
| CompletableFuture<Long> future; | ||
| if (producer.getTopicEpoch().isPresent()) { | ||
| future = setTopicEpoch(producer.getTopicEpoch().get()); | ||
| } else { | ||
| future = incrementTopicEpoch(topicEpoch); | ||
| } | ||
| return future.thenApply(epoch -> { | ||
| topicEpoch = Optional.of(epoch); | ||
| return topicEpoch; | ||
| }).exceptionally(ex -> { | ||
| hasExclusiveProducer = false; | ||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| // case WaitForExclusive: | ||
| // TODO: Implementation | ||
|
|
||
| default: | ||
| return FutureUtil.failedFuture( | ||
| new BrokerServiceException("Invalid producer access mode: " + producer.getAccessMode())); | ||
| } | ||
|
|
||
| } finally { | ||
| lock.writeLock().unlock(); | ||
| } | ||
| } | ||
|
|
||
| protected abstract CompletableFuture<Long> setTopicEpoch(long newEpoch); | ||
|
|
||
| protected abstract CompletableFuture<Long> incrementTopicEpoch(Optional<Long> currentEpoch); | ||
|
|
||
| @Override | ||
| public void recordAddLatency(long latency, TimeUnit unit) { | ||
| addEntryLatencyStatsUsec.addValue(unit.toMicros(latency)); | ||
|
|
@@ -450,7 +565,44 @@ private boolean isUserProvidedProducerName(Producer producer){ | |
| return producer.isUserProvidedProducerName() && !producer.getProducerName().startsWith(replicatorPrefix); | ||
| } | ||
|
|
||
| protected abstract void handleProducerRemoved(Producer producer); | ||
|
|
||
| @Override | ||
| public void removeProducer(Producer producer) { | ||
| checkArgument(producer.getTopic() == this); | ||
|
|
||
| if (producers.remove(producer.getProducerName(), producer)) { | ||
| handleProducerRemoved(producer); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we need lock here else it may create a race condition and producer with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No lock needed there so far. Other changes are needed for |
||
| } | ||
| } | ||
|
|
||
| protected void handleProducerRemoved(Producer producer) { | ||
| // decrement usage only if this was a valid producer close | ||
| long newCount = USAGE_COUNT_UPDATER.decrementAndGet(this); | ||
| if (newCount == 0) { | ||
| hasExclusiveProducer = false; | ||
| } | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[{}] [{}] Removed producer -- count: {}", topic, producer.getProducerName(), | ||
| USAGE_COUNT_UPDATER.get(this)); | ||
| } | ||
| lastActive = System.nanoTime(); | ||
| } | ||
|
|
||
| public void handleConsumerAdded(String subscriptionName, String consumerName) { | ||
| USAGE_COUNT_UPDATER.incrementAndGet(this); | ||
| if (log.isDebugEnabled()) { | ||
| log.debug("[{}] [{}] [{}] Added consumer -- count: {}", topic, subscriptionName, | ||
| consumerName, USAGE_COUNT_UPDATER.get(this)); | ||
| } | ||
| } | ||
|
|
||
| public void decrementUsageCount() { | ||
| USAGE_COUNT_UPDATER.decrementAndGet(this); | ||
| } | ||
|
|
||
| public long currentUsageCount() { | ||
| return usageCount; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isPublishRateExceeded() { | ||
|
|
@@ -536,6 +688,8 @@ public void setDeleteWhileInactive(boolean deleteWhileInactive) { | |
| this.inactiveTopicPolicies.setDeleteWhileInactive(deleteWhileInactive); | ||
| } | ||
|
|
||
| protected abstract boolean isTerminated(); | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(AbstractTopic.class); | ||
|
|
||
| public InactiveTopicPolicies getInactiveTopicPolicies() { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.