|
50 | 50 | import java.util.concurrent.atomic.AtomicLongFieldUpdater; |
51 | 51 | import java.util.concurrent.atomic.LongAdder; |
52 | 52 | import java.util.concurrent.locks.ReentrantReadWriteLock; |
| 53 | +import java.util.function.Function; |
53 | 54 | import java.util.function.ToLongFunction; |
54 | 55 | import lombok.Getter; |
55 | 56 | import lombok.Setter; |
|
60 | 61 | import org.apache.commons.lang3.tuple.Pair; |
61 | 62 | import org.apache.pulsar.broker.PulsarService; |
62 | 63 | import org.apache.pulsar.broker.ServiceConfiguration; |
| 64 | +import org.apache.pulsar.broker.loadbalance.extensions.ExtensibleLoadManagerImpl; |
63 | 65 | import org.apache.pulsar.broker.resourcegroup.ResourceGroup; |
64 | 66 | import org.apache.pulsar.broker.resourcegroup.ResourceGroupPublishLimiter; |
65 | 67 | import org.apache.pulsar.broker.resources.NamespaceResources; |
@@ -119,6 +121,10 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener { |
119 | 121 |
|
120 | 122 | protected final BrokerService brokerService; |
121 | 123 |
|
| 124 | + // Wraps this topic as a TopicPolicyListener so topic-policy updates received while the initial policy is still |
| 125 | + // loading are buffered and applied in order once initTopicPolicy() completes initialization. |
| 126 | + protected final TopicPolicyListenerWrapper topicPolicyListener = new TopicPolicyListenerWrapper(this); |
| 127 | + |
122 | 128 | // Prefix for replication cursors |
123 | 129 | protected final String replicatorPrefix; |
124 | 130 |
|
@@ -180,7 +186,17 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener { |
180 | 186 | AtomicLongFieldUpdater.newUpdater(AbstractTopic.class, "usageCount"); |
181 | 187 | private volatile long usageCount = 0; |
182 | 188 |
|
183 | | - private Map<String/*subscription*/, SubscriptionPolicies> subscriptionPolicies = Collections.emptyMap(); |
| 189 | + // Effective per-subscription policies, merged from the local and global topic policies with local precedence. |
| 190 | + // Unlike the PolicyHierarchyValue-backed fields, subscriptionPolicies is a plain map. It is kept as the merge of |
| 191 | + // the two scopes below (rather than assigned directly) because the local-before-global initialization order |
| 192 | + // (see TopicPolicyListenerWrapper) would otherwise let the global map -- empty by default in TopicPolicies -- |
| 193 | + // overwrite and clear the local per-subscription policies that were applied just before it. |
| 194 | + // subscriptionPolicies is volatile because it is read on dispatch threads (getSubscriptionDispatchRate) while it |
| 195 | + // is updated on the policy-update thread. localSubscriptionPolicies/globalSubscriptionPolicies are only ever read |
| 196 | + // and written on the (single) policy-update thread, so they don't need to be volatile. |
| 197 | + private volatile Map<String/*subscription*/, SubscriptionPolicies> subscriptionPolicies = Collections.emptyMap(); |
| 198 | + private Map<String/*subscription*/, SubscriptionPolicies> localSubscriptionPolicies = Collections.emptyMap(); |
| 199 | + private Map<String/*subscription*/, SubscriptionPolicies> globalSubscriptionPolicies = Collections.emptyMap(); |
184 | 200 |
|
185 | 201 | protected final LongAdder msgOutFromRemovedSubscriptions = new LongAdder(); |
186 | 202 | protected final LongAdder bytesOutFromRemovedSubscriptions = new LongAdder(); |
@@ -320,11 +336,36 @@ protected void updateTopicPolicy(TopicPolicies data) { |
320 | 336 | topicPolicies.getEntryFilters().updateTopicValue(data.getEntryFilters(), isGlobalPolicies); |
321 | 337 | topicPolicies.getDispatcherPauseOnAckStatePersistentEnabled() |
322 | 338 | .updateTopicValue(data.getDispatcherPauseOnAckStatePersistentEnabled(), isGlobalPolicies); |
323 | | - this.subscriptionPolicies = data.getSubscriptionPolicies(); |
| 339 | + |
| 340 | + // Merge instead of assigning directly: keep the local and global per-subscription policies separately and |
| 341 | + // recompute the effective map with local precedence, so applying the (default-empty) global map does not |
| 342 | + // clear the local per-subscription policies during the local-before-global initialization. |
| 343 | + if (isGlobalPolicies) { |
| 344 | + globalSubscriptionPolicies = data.getSubscriptionPolicies(); |
| 345 | + } else { |
| 346 | + localSubscriptionPolicies = data.getSubscriptionPolicies(); |
| 347 | + } |
| 348 | + subscriptionPolicies = mergeSubscriptionPolicies(globalSubscriptionPolicies, localSubscriptionPolicies); |
324 | 349 |
|
325 | 350 | updateEntryFilters(); |
326 | 351 | } |
327 | 352 |
|
| 353 | + // Merges the global and local per-subscription policies with local precedence: a subscription present in the |
| 354 | + // local policies keeps its local value; otherwise the global value (if any) is used. |
| 355 | + private static Map<String, SubscriptionPolicies> mergeSubscriptionPolicies( |
| 356 | + Map<String, SubscriptionPolicies> globalSubscriptionPolicies, |
| 357 | + Map<String, SubscriptionPolicies> localSubscriptionPolicies) { |
| 358 | + if (globalSubscriptionPolicies.isEmpty()) { |
| 359 | + return localSubscriptionPolicies; |
| 360 | + } |
| 361 | + if (localSubscriptionPolicies.isEmpty()) { |
| 362 | + return globalSubscriptionPolicies; |
| 363 | + } |
| 364 | + Map<String, SubscriptionPolicies> merged = new HashMap<>(globalSubscriptionPolicies); |
| 365 | + merged.putAll(localSubscriptionPolicies); |
| 366 | + return merged; |
| 367 | + } |
| 368 | + |
328 | 369 | protected void updateTopicPolicyByNamespacePolicy(Policies namespacePolicies) { |
329 | 370 | log.debug() |
330 | 371 | .attr("namespacePolicies", namespacePolicies) |
@@ -566,19 +607,68 @@ protected boolean isProducersExceeded(boolean isRemote) { |
566 | 607 | } |
567 | 608 |
|
568 | 609 | protected TopicPolicyListener getTopicPolicyListener() { |
569 | | - return this; |
570 | | - } |
571 | | - |
572 | | - protected void registerTopicPolicyListener() { |
573 | | - brokerService.getPulsar().getTopicPoliciesService() |
574 | | - .registerListenerAsync(TopicName.getPartitionedTopicName(topic), getTopicPolicyListener()); |
| 610 | + return topicPolicyListener; |
575 | 611 | } |
576 | 612 |
|
577 | 613 | protected void unregisterTopicPolicyListener() { |
578 | 614 | brokerService.getPulsar().getTopicPoliciesService() |
579 | 615 | .unregisterListener(TopicName.getPartitionedTopicName(topic), getTopicPolicyListener()); |
580 | 616 | } |
581 | 617 |
|
| 618 | + /** |
| 619 | + * Registers the topic-policy listener and applies the topic's initial policies (global and local) to this topic. |
| 620 | + * Shared by {@link org.apache.pulsar.broker.service.persistent.PersistentTopic} and |
| 621 | + * {@link org.apache.pulsar.broker.service.nonpersistent.NonPersistentTopic} so both load their own policies on |
| 622 | + * topic load, which removes the need to broadcast every topic's policy when a namespace's policy cache finishes |
| 623 | + * loading (see {@code topicPolicyListenerReplayEnabled}). |
| 624 | + * |
| 625 | + * <p>Each call re-initializes the listener wrapper and, whatever the outcome, always completes its initialization |
| 626 | + * afterwards, so the wrapper never stays in the buffering phase (dropping updates) even if policy loading fails. |
| 627 | + * This makes the method safe to run again (e.g. a future retry); runs are expected to be serialized. |
| 628 | + */ |
| 629 | + protected CompletableFuture<Void> initTopicPolicy() { |
| 630 | + final var topicPoliciesService = brokerService.getPulsar().getTopicPoliciesService(); |
| 631 | + final var partitionedTopicName = TopicName.getPartitionedTopicName(topic); |
| 632 | + |
| 633 | + // Begin a fresh initialization phase: updates are buffered until initialization completes below. This resets |
| 634 | + // any previous phase so the method can be run again. |
| 635 | + topicPolicyListener.startInitialization(); |
| 636 | + CompletableFuture<Void> initTopicPolicyFuture = |
| 637 | + topicPoliciesService.registerListenerAsync(partitionedTopicName, topicPolicyListener) |
| 638 | + .thenCompose(registered -> { |
| 639 | + if (!registered) { |
| 640 | + return CompletableFuture.completedFuture(null); |
| 641 | + } |
| 642 | + if (ExtensibleLoadManagerImpl.isInternalTopic(topic)) { |
| 643 | + // Internal topics don't load topic-level policies |
| 644 | + return CompletableFuture.completedFuture(null); |
| 645 | + } |
| 646 | + // future for fetching global topic policies |
| 647 | + CompletableFuture<Optional<TopicPolicies>> globalPoliciesFuture = |
| 648 | + topicPoliciesService.getTopicPoliciesAsync(partitionedTopicName, |
| 649 | + TopicPoliciesService.GetType.GLOBAL_ONLY); |
| 650 | + // future for fetching local topic policies |
| 651 | + CompletableFuture<Optional<TopicPolicies>> localPoliciesFuture = |
| 652 | + topicPoliciesService.getTopicPoliciesAsync(partitionedTopicName, |
| 653 | + TopicPoliciesService.GetType.LOCAL_ONLY); |
| 654 | + return globalPoliciesFuture.thenCombine(localPoliciesFuture, (global, local) -> { |
| 655 | + // finally update the topic policies with the latest value or loaded value |
| 656 | + return CompletableFuture.runAsync(() -> |
| 657 | + topicPolicyListener.completeInitialization(global.orElse(null), |
| 658 | + local.orElse(null)), |
| 659 | + getPoliciesNotifyThread()); |
| 660 | + }).thenCompose(Function.identity()); |
| 661 | + }); |
| 662 | + // Whatever the outcome -- success, failure, or the listener not being registered -- make sure the wrapper |
| 663 | + // leaves the initialization (buffering) phase, so it forwards any buffered value plus all future live updates |
| 664 | + // instead of dropping them. This is a no-op when the loaded policies were already applied above. Return the |
| 665 | + // whenComplete stage (not initTopicPolicyFuture) so the returned future completes only after this has run, and |
| 666 | + // whenComplete's pass-through semantics carry the original success or failure to the caller's initialize(). |
| 667 | + return initTopicPolicyFuture.whenCompleteAsync((v, ex) -> { |
| 668 | + topicPolicyListener.completeInitializationUnlessAlreadyCompleted(); |
| 669 | + }, getPoliciesNotifyThread()); |
| 670 | + } |
| 671 | + |
582 | 672 | protected boolean isSameAddressProducersExceeded(Producer producer) { |
583 | 673 | if (isSystemTopic() || producer.isRemote()) { |
584 | 674 | return false; |
|
0 commit comments