|
31 | 31 | import java.util.Collection; |
32 | 32 | import java.util.Collections; |
33 | 33 | import java.util.EnumSet; |
| 34 | +import java.util.HashMap; |
34 | 35 | import java.util.List; |
35 | 36 | import java.util.Map; |
36 | 37 | import java.util.Objects; |
|
48 | 49 | import java.util.concurrent.atomic.AtomicLongFieldUpdater; |
49 | 50 | import java.util.concurrent.atomic.LongAdder; |
50 | 51 | import java.util.concurrent.locks.ReentrantReadWriteLock; |
| 52 | +import java.util.function.Function; |
51 | 53 | import java.util.function.ToLongFunction; |
52 | 54 | import lombok.Getter; |
53 | 55 | import lombok.Setter; |
|
58 | 60 | import org.apache.commons.lang3.tuple.Pair; |
59 | 61 | import org.apache.pulsar.broker.PulsarService; |
60 | 62 | import org.apache.pulsar.broker.ServiceConfiguration; |
| 63 | +import org.apache.pulsar.broker.loadbalance.extensions.ExtensibleLoadManagerImpl; |
61 | 64 | import org.apache.pulsar.broker.resourcegroup.ResourceGroup; |
62 | 65 | import org.apache.pulsar.broker.resourcegroup.ResourceGroupPublishLimiter; |
63 | 66 | import org.apache.pulsar.broker.service.BrokerServiceException.ConsumerBusyException; |
@@ -114,6 +117,10 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener { |
114 | 117 |
|
115 | 118 | protected final BrokerService brokerService; |
116 | 119 |
|
| 120 | + // Wraps this topic as a TopicPolicyListener so topic-policy updates received while the initial policy is still |
| 121 | + // loading are buffered and applied in order once initTopicPolicy() completes initialization. |
| 122 | + protected final TopicPolicyListenerWrapper topicPolicyListener = new TopicPolicyListenerWrapper(this); |
| 123 | + |
117 | 124 | // Prefix for replication cursors |
118 | 125 | protected final String replicatorPrefix; |
119 | 126 |
|
@@ -173,7 +180,17 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener { |
173 | 180 | AtomicLongFieldUpdater.newUpdater(AbstractTopic.class, "usageCount"); |
174 | 181 | private volatile long usageCount = 0; |
175 | 182 |
|
176 | | - private Map<String/*subscription*/, SubscriptionPolicies> subscriptionPolicies = Collections.emptyMap(); |
| 183 | + // Effective per-subscription policies, merged from the local and global topic policies with local precedence. |
| 184 | + // Unlike the PolicyHierarchyValue-backed fields, subscriptionPolicies is a plain map. It is kept as the merge of |
| 185 | + // the two scopes below (rather than assigned directly) because the local-before-global initialization order |
| 186 | + // (see TopicPolicyListenerWrapper) would otherwise let the global map -- empty by default in TopicPolicies -- |
| 187 | + // overwrite and clear the local per-subscription policies that were applied just before it. |
| 188 | + // subscriptionPolicies is volatile because it is read on dispatch threads (getSubscriptionDispatchRate) while it |
| 189 | + // is updated on the policy-update thread. localSubscriptionPolicies/globalSubscriptionPolicies are only ever read |
| 190 | + // and written on the (single) policy-update thread, so they don't need to be volatile. |
| 191 | + private volatile Map<String/*subscription*/, SubscriptionPolicies> subscriptionPolicies = Collections.emptyMap(); |
| 192 | + private Map<String/*subscription*/, SubscriptionPolicies> localSubscriptionPolicies = Collections.emptyMap(); |
| 193 | + private Map<String/*subscription*/, SubscriptionPolicies> globalSubscriptionPolicies = Collections.emptyMap(); |
177 | 194 |
|
178 | 195 | protected final LongAdder msgOutFromRemovedSubscriptions = new LongAdder(); |
179 | 196 | protected final LongAdder bytesOutFromRemovedSubscriptions = new LongAdder(); |
@@ -310,11 +327,36 @@ protected void updateTopicPolicy(TopicPolicies data) { |
310 | 327 | topicPolicies.getEntryFilters().updateTopicValue(data.getEntryFilters(), isGlobalPolicies); |
311 | 328 | topicPolicies.getDispatcherPauseOnAckStatePersistentEnabled() |
312 | 329 | .updateTopicValue(data.getDispatcherPauseOnAckStatePersistentEnabled(), isGlobalPolicies); |
313 | | - this.subscriptionPolicies = data.getSubscriptionPolicies(); |
| 330 | + |
| 331 | + // Merge instead of assigning directly: keep the local and global per-subscription policies separately and |
| 332 | + // recompute the effective map with local precedence, so applying the (default-empty) global map does not |
| 333 | + // clear the local per-subscription policies during the local-before-global initialization. |
| 334 | + if (isGlobalPolicies) { |
| 335 | + globalSubscriptionPolicies = data.getSubscriptionPolicies(); |
| 336 | + } else { |
| 337 | + localSubscriptionPolicies = data.getSubscriptionPolicies(); |
| 338 | + } |
| 339 | + subscriptionPolicies = mergeSubscriptionPolicies(globalSubscriptionPolicies, localSubscriptionPolicies); |
314 | 340 |
|
315 | 341 | updateEntryFilters(); |
316 | 342 | } |
317 | 343 |
|
| 344 | + // Merges the global and local per-subscription policies with local precedence: a subscription present in the |
| 345 | + // local policies keeps its local value; otherwise the global value (if any) is used. |
| 346 | + private static Map<String, SubscriptionPolicies> mergeSubscriptionPolicies( |
| 347 | + Map<String, SubscriptionPolicies> globalSubscriptionPolicies, |
| 348 | + Map<String, SubscriptionPolicies> localSubscriptionPolicies) { |
| 349 | + if (globalSubscriptionPolicies.isEmpty()) { |
| 350 | + return localSubscriptionPolicies; |
| 351 | + } |
| 352 | + if (localSubscriptionPolicies.isEmpty()) { |
| 353 | + return globalSubscriptionPolicies; |
| 354 | + } |
| 355 | + Map<String, SubscriptionPolicies> merged = new HashMap<>(globalSubscriptionPolicies); |
| 356 | + merged.putAll(localSubscriptionPolicies); |
| 357 | + return merged; |
| 358 | + } |
| 359 | + |
318 | 360 | protected void updateTopicPolicyByNamespacePolicy(Policies namespacePolicies) { |
319 | 361 | if (log.isDebugEnabled()) { |
320 | 362 | log.debug("[{}]updateTopicPolicyByNamespacePolicy,data={}", topic, namespacePolicies); |
@@ -550,19 +592,68 @@ protected boolean isProducersExceeded(boolean isRemote) { |
550 | 592 | } |
551 | 593 |
|
552 | 594 | protected TopicPolicyListener getTopicPolicyListener() { |
553 | | - return this; |
554 | | - } |
555 | | - |
556 | | - protected void registerTopicPolicyListener() { |
557 | | - brokerService.getPulsar().getTopicPoliciesService() |
558 | | - .registerListenerAsync(TopicName.getPartitionedTopicName(topic), getTopicPolicyListener()); |
| 595 | + return topicPolicyListener; |
559 | 596 | } |
560 | 597 |
|
561 | 598 | protected void unregisterTopicPolicyListener() { |
562 | 599 | brokerService.getPulsar().getTopicPoliciesService() |
563 | 600 | .unregisterListener(TopicName.getPartitionedTopicName(topic), getTopicPolicyListener()); |
564 | 601 | } |
565 | 602 |
|
| 603 | + /** |
| 604 | + * Registers the topic-policy listener and applies the topic's initial policies (global and local) to this topic. |
| 605 | + * Shared by {@link org.apache.pulsar.broker.service.persistent.PersistentTopic} and |
| 606 | + * {@link org.apache.pulsar.broker.service.nonpersistent.NonPersistentTopic} so both load their own policies on |
| 607 | + * topic load, which removes the need to broadcast every topic's policy when a namespace's policy cache finishes |
| 608 | + * loading (see {@code topicPolicyListenerReplayEnabled}). |
| 609 | + * |
| 610 | + * <p>Each call re-initializes the listener wrapper and, whatever the outcome, always completes its initialization |
| 611 | + * afterwards, so the wrapper never stays in the buffering phase (dropping updates) even if policy loading fails. |
| 612 | + * This makes the method safe to run again (e.g. a future retry); runs are expected to be serialized. |
| 613 | + */ |
| 614 | + protected CompletableFuture<Void> initTopicPolicy() { |
| 615 | + final var topicPoliciesService = brokerService.getPulsar().getTopicPoliciesService(); |
| 616 | + final var partitionedTopicName = TopicName.getPartitionedTopicName(topic); |
| 617 | + |
| 618 | + // Begin a fresh initialization phase: updates are buffered until initialization completes below. This resets |
| 619 | + // any previous phase so the method can be run again. |
| 620 | + topicPolicyListener.startInitialization(); |
| 621 | + CompletableFuture<Void> initTopicPolicyFuture = |
| 622 | + topicPoliciesService.registerListenerAsync(partitionedTopicName, topicPolicyListener) |
| 623 | + .thenCompose(registered -> { |
| 624 | + if (!registered) { |
| 625 | + return CompletableFuture.completedFuture(null); |
| 626 | + } |
| 627 | + if (ExtensibleLoadManagerImpl.isInternalTopic(topic)) { |
| 628 | + // Internal topics don't load topic-level policies |
| 629 | + return CompletableFuture.completedFuture(null); |
| 630 | + } |
| 631 | + // future for fetching global topic policies |
| 632 | + CompletableFuture<Optional<TopicPolicies>> globalPoliciesFuture = |
| 633 | + topicPoliciesService.getTopicPoliciesAsync(partitionedTopicName, |
| 634 | + TopicPoliciesService.GetType.GLOBAL_ONLY); |
| 635 | + // future for fetching local topic policies |
| 636 | + CompletableFuture<Optional<TopicPolicies>> localPoliciesFuture = |
| 637 | + topicPoliciesService.getTopicPoliciesAsync(partitionedTopicName, |
| 638 | + TopicPoliciesService.GetType.LOCAL_ONLY); |
| 639 | + return globalPoliciesFuture.thenCombine(localPoliciesFuture, (global, local) -> { |
| 640 | + // finally update the topic policies with the latest value or loaded value |
| 641 | + return CompletableFuture.runAsync(() -> |
| 642 | + topicPolicyListener.completeInitialization(global.orElse(null), |
| 643 | + local.orElse(null)), |
| 644 | + getPoliciesNotifyThread()); |
| 645 | + }).thenCompose(Function.identity()); |
| 646 | + }); |
| 647 | + // Whatever the outcome -- success, failure, or the listener not being registered -- make sure the wrapper |
| 648 | + // leaves the initialization (buffering) phase, so it forwards any buffered value plus all future live updates |
| 649 | + // instead of dropping them. This is a no-op when the loaded policies were already applied above. Return the |
| 650 | + // whenComplete stage (not initTopicPolicyFuture) so the returned future completes only after this has run, and |
| 651 | + // whenComplete's pass-through semantics carry the original success or failure to the caller's initialize(). |
| 652 | + return initTopicPolicyFuture.whenCompleteAsync((v, ex) -> { |
| 653 | + topicPolicyListener.completeInitializationUnlessAlreadyCompleted(); |
| 654 | + }, getPoliciesNotifyThread()); |
| 655 | + } |
| 656 | + |
566 | 657 | protected boolean isSameAddressProducersExceeded(Producer producer) { |
567 | 658 | if (isSystemTopic() || producer.isRemote()) { |
568 | 659 | return false; |
|
0 commit comments