Skip to content

Commit 0629dc5

Browse files
authored
[improve][broker] Load topic policies on non-persistent topic load and gate the policy replay (#26134)
1 parent decc80f commit 0629dc5

11 files changed

Lines changed: 450 additions & 102 deletions

File tree

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,6 +1992,15 @@ The max allowed delay for delayed delivery (in milliseconds). If the broker rece
19921992
+ "please enable the system topic first.")
19931993
private boolean topicLevelPoliciesEnabled = true;
19941994

1995+
@FieldContext(
1996+
category = CATEGORY_SERVER,
1997+
doc = "When enabled, all registered topic-policy listeners in a namespace are re-notified with the current"
1998+
+ " topic policies after the namespace's topic-policy cache finishes its initial load. Topics load"
1999+
+ " and apply their own policies when they are loaded, so this broadcast is normally redundant; it"
2000+
+ " is only needed for custom plugins that register TopicPolicyListeners and depend on it for"
2001+
+ " backwards compatibility. Disabled by default.")
2002+
private boolean topicPolicyListenerReplayEnabled = false;
2003+
19952004
@FieldContext(
19962005
category = CATEGORY_SERVER,
19972006
doc = """

pulsar-broker/src/main/java/org/apache/pulsar/broker/service/AbstractTopic.java

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
5151
import java.util.concurrent.atomic.LongAdder;
5252
import java.util.concurrent.locks.ReentrantReadWriteLock;
53+
import java.util.function.Function;
5354
import java.util.function.ToLongFunction;
5455
import lombok.Getter;
5556
import lombok.Setter;
@@ -60,6 +61,7 @@
6061
import org.apache.commons.lang3.tuple.Pair;
6162
import org.apache.pulsar.broker.PulsarService;
6263
import org.apache.pulsar.broker.ServiceConfiguration;
64+
import org.apache.pulsar.broker.loadbalance.extensions.ExtensibleLoadManagerImpl;
6365
import org.apache.pulsar.broker.resourcegroup.ResourceGroup;
6466
import org.apache.pulsar.broker.resourcegroup.ResourceGroupPublishLimiter;
6567
import org.apache.pulsar.broker.resources.NamespaceResources;
@@ -119,6 +121,10 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener {
119121

120122
protected final BrokerService brokerService;
121123

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+
122128
// Prefix for replication cursors
123129
protected final String replicatorPrefix;
124130

@@ -180,7 +186,17 @@ public abstract class AbstractTopic implements Topic, TopicPolicyListener {
180186
AtomicLongFieldUpdater.newUpdater(AbstractTopic.class, "usageCount");
181187
private volatile long usageCount = 0;
182188

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();
184200

185201
protected final LongAdder msgOutFromRemovedSubscriptions = new LongAdder();
186202
protected final LongAdder bytesOutFromRemovedSubscriptions = new LongAdder();
@@ -320,11 +336,36 @@ protected void updateTopicPolicy(TopicPolicies data) {
320336
topicPolicies.getEntryFilters().updateTopicValue(data.getEntryFilters(), isGlobalPolicies);
321337
topicPolicies.getDispatcherPauseOnAckStatePersistentEnabled()
322338
.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);
324349

325350
updateEntryFilters();
326351
}
327352

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+
328369
protected void updateTopicPolicyByNamespacePolicy(Policies namespacePolicies) {
329370
log.debug()
330371
.attr("namespacePolicies", namespacePolicies)
@@ -566,19 +607,68 @@ protected boolean isProducersExceeded(boolean isRemote) {
566607
}
567608

568609
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;
575611
}
576612

577613
protected void unregisterTopicPolicyListener() {
578614
brokerService.getPulsar().getTopicPoliciesService()
579615
.unregisterListener(TopicName.getPartitionedTopicName(topic), getTopicPolicyListener());
580616
}
581617

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+
582672
protected boolean isSameAddressProducersExceeded(Producer producer) {
583673
if (isSystemTopic() || producer.isRemote()) {
584674
return false;

pulsar-broker/src/main/java/org/apache/pulsar/broker/service/SystemTopicBasedTopicPoliciesService.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -892,18 +892,43 @@ private void initPolicesCache(SystemTopicClient.Reader<PulsarEvent> reader, Comp
892892
.attr("topic", reader.getSystemTopic().getTopicName())
893893
.log("Reach the end of the system topic.");
894894

895-
// replay policy message
896-
List<CompletableFuture<Void>> notifyFutures = new ArrayList<>();
897-
for (Map.Entry<TopicName, TopicPolicies> entry : policiesCache.entrySet()) {
898-
TopicName topicName = entry.getKey();
899-
TopicPolicies policies = entry.getValue();
900-
notifyFutures.add(notifyListenersForTopicAsync(topicName, policies));
895+
// Optionally re-notify the topic-policy listeners for this namespace with the cached policies.
896+
// Topics load their own policies on load (AbstractTopic#initTopicPolicy), so this replay is only
897+
// needed for custom plugins that register TopicPolicyListeners and rely on the broadcast; it is
898+
// off by default (topicPolicyListenerReplayEnabled).
899+
if (pulsarService.getConfiguration().isTopicPolicyListenerReplayEnabled()) {
900+
NamespaceName namespaceObject = reader.getSystemTopic().getTopicName().getNamespaceObject();
901+
FutureUtil.completeAfter(future, replayTopicPolicyListeners(namespaceObject));
902+
} else {
903+
future.complete(null);
901904
}
902-
FutureUtil.completeAfter(future, FutureUtil.waitForAll(notifyFutures));
903905
}
904906
});
905907
}
906908

909+
/**
910+
* Re-notifies the registered topic-policy listeners for every topic in {@code namespace} with the currently
911+
* cached policies (both local and global). Topics apply their own policies on load, so this is only needed for
912+
* custom plugins that register {@link TopicPolicyListener}s and rely on the broadcast when a namespace's policy
913+
* cache finishes loading (see {@code topicPolicyListenerReplayEnabled}).
914+
*/
915+
@VisibleForTesting
916+
CompletableFuture<Void> replayTopicPolicyListeners(NamespaceName namespace) {
917+
List<CompletableFuture<Void>> notifyFutures = new ArrayList<>();
918+
addNamespacePolicyNotifications(policiesCache, namespace, notifyFutures);
919+
addNamespacePolicyNotifications(globalPoliciesCache, namespace, notifyFutures);
920+
return FutureUtil.waitForAll(notifyFutures);
921+
}
922+
923+
private void addNamespacePolicyNotifications(Map<TopicName, TopicPolicies> cache, NamespaceName namespace,
924+
List<CompletableFuture<Void>> notifyFutures) {
925+
for (Map.Entry<TopicName, TopicPolicies> entry : cache.entrySet()) {
926+
if (Objects.equals(entry.getKey().getNamespaceObject(), namespace)) {
927+
notifyFutures.add(notifyListenersForTopicAsync(entry.getKey(), entry.getValue()));
928+
}
929+
}
930+
}
931+
907932
// Full teardown of a namespace's topic-policies state: removes and closes the reader, the message-handler
908933
// tracker, the cached policies and the init future. Used when the whole namespace is unloaded.
909934
@VisibleForTesting

0 commit comments

Comments
 (0)