Skip to content

[fix] [broker] Ensure policy cache is updated before updateTopicPoliciesAsync returns.#21030

Closed
thetumbled wants to merge 3 commits into
apache:masterfrom
thetumbled:Fix_ConcurrentModificationPolicy
Closed

[fix] [broker] Ensure policy cache is updated before updateTopicPoliciesAsync returns.#21030
thetumbled wants to merge 3 commits into
apache:masterfrom
thetumbled:Fix_ConcurrentModificationPolicy

Conversation

@thetumbled

@thetumbled thetumbled commented Aug 19, 2023

Copy link
Copy Markdown
Member

Motivation

Calling these two method synchronously to set the retention and ttl of one topic will result into unexpected results.

admin.topics().setRetention
admin.topics().setMessageTTL

For example, if we call

int retentionTimeInMinutes = 2880;
int messageTTLInSecond = retentionTimeInMinutes * 60;
admin.topics().setRetention(topic,new RetentionPolicies(retentionTimeInMinute, -1));
admin.topics().setMessageTTL(topic, messageTTLInSecond);

We expect that the retention is set to 2880 minutes, ttl is set to 2880*16=172800 seconds. But the result is that the retention policy is null !
We read the messages from __change_events derectly and find out the reason:

TimeStamp:1692157480789 ,Message received: PulsarEvent(eventType=TOPIC_POLICY, actionType=UPDATE, topicPoliciesEvent=TopicPoliciesEvent(, policies=TopicPolicies(backLogQuotaMap={}, subscriptionTypesEnabled=[], persistence=null, **retentionPolicies=RetentionPolicies{retentionTimeInMinutes=2880, retentionSizeInMB=-1},** deduplicationEnabled=null, **messageTTLInSeconds=null**, maxProducerPerTopic=null, maxConsumerPerTopic=null, maxConsumersPerSubscription=null, maxUnackedMessagesOnConsumer=null, maxUnackedMessagesOnSubscription=null, delayedDeliveryTickTimeMillis=null, delayedDeliveryEnabled=null, offloadPolicies=null, inactiveTopicPolicies=null, dispatchRate=null, subscriptionDispatchRate=null, compactionThreshold=null, publishRate=null, subscribeRate=null, deduplicationSnapshotIntervalSeconds=null, maxMessageSize=null, maxSubscriptionsPerTopic=null, replicatorDispatchRate=null)))

TimeStamp:1692157480811 ,Message received: PulsarEvent(eventType=TOPIC_POLICY, actionType=UPDATE, topicPoliciesEvent=TopicPoliciesEvent(, policies=TopicPolicies(backLogQuotaMap={}, subscriptionTypesEnabled=[], persistence=null, **retentionPolicies=null**, deduplicationEnabled=null, **messageTTLInSeconds=172800**, maxProducerPerTopic=null, maxConsumerPerTopic=null, maxConsumersPerSubscription=null, maxUnackedMessagesOnConsumer=null, maxUnackedMessagesOnSubscription=null, delayedDeliveryTickTimeMillis=null, delayedDeliveryEnabled=null, offloadPolicies=null, inactiveTopicPolicies=null, dispatchRate=null, subscriptionDispatchRate=null, compactionThreshold=null, publishRate=null, subscribeRate=null, deduplicationSnapshotIntervalSeconds=null, maxMessageSize=null, maxSubscriptionsPerTopic=null, replicatorDispatchRate=null)))

We can see that the retention policy in the second message is null, which result into the null retention policy.

Diving into the implementation of SystemTopicBasedTopicPoliciesService, we can see that org.apache.pulsar.broker.service.SystemTopicBasedTopicPoliciesService#updateTopicPoliciesAsync returns as soon as the message is written into __change_events, but do not guranteen that the topic policy in cache is updated. However, topic policy setting api will retrieve the topic policy from cache.

org.apache.pulsar.broker.admin.v2.PersistentTopics#setRetention
org.apache.pulsar.broker.admin.v2.PersistentTopics#setMessageTTL

Before the cache is updated, the second rest api for setting ttl arrived, and change the topic policy based on the old one, which result into the problem.

Modifications

The solution is: Ensure policy cache is updated before updateTopicPoliciesAsync returns.

Verifying this change

  • Make sure that the change passes the CI checks.

(Please pick either of the following options)

This change added tests and can be verified as follows:

Does this pull request potentially affect one of the following parts:

If the box was checked, please highlight the changes

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

Documentation

  • doc
  • doc-required
  • doc-not-needed
  • doc-complete

Matching PR in forked repository

PR in forked repository: thetumbled#25

@github-actions

Copy link
Copy Markdown

@thetumbled Please add the following content to your PR description and select a checkbox:

- [ ] `doc` <!-- Your PR contains doc changes -->
- [ ] `doc-required` <!-- Your PR changes impact docs and you will update later -->
- [ ] `doc-not-needed` <!-- Your PR changes do not impact docs -->
- [ ] `doc-complete` <!-- Docs have been already added -->

.thenAccept(msg -> {
refreshTopicPoliciesCache(msg);
notifyListener(msg);
completeResults(TopicName.get(TopicName.get(msg.getKey()).getPartitionedTopicName()));

@AnonHxy AnonHxy Aug 19, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think this is a right way to solve the problem. For examle:

If we have three different actions, e.g., setA and setB and setC:

  1. setA is waiting for futureA completed. The eventMsg is eventA.
  2. setB is waiting for futureB completed. The eventMsg is eventB.
  3. setC is waiting for futureC completed. The eventMsg is eventC.
  4. The method completeResults will complete futureA and futureB and futureC when read eventA. Howerver setB has not been refresh to the policies cache.
  5. So setC maybe read the old B value

@thetumbled thetumbled Aug 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the corner case for this solution, which can not handle with concurrent request for modifying topic policy. This solution fix the synchronize case.
And i wonder whether we have the need to implement strong concurrent controll? There are two directions:

  1. add a warning to users, that the topic policy setting api can not handle with concurrent cases
  2. or, i can implement a stronger concurrent controll.
    What do you think? @AnonHxy @Demogorgon314 @codelipenghui

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I see. Synchronize case is enough I think

@thetumbled thetumbled requested a review from AnonHxy August 21, 2023 02:07
@codelipenghui codelipenghui added this to the 3.2.0 milestone Aug 21, 2023
@codelipenghui codelipenghui added the type/bug The PR fixed a bug or issue reported a bug label Aug 21, 2023
@thetumbled thetumbled requested a review from AnonHxy August 21, 2023 08:23
@VisibleForTesting
final Map<TopicName, List<TopicPolicyListener<TopicPolicies>>> listeners = new ConcurrentHashMap<>();

final Map<TopicName, BlockingDeque<CompletableFuture>> results = new ConcurrentHashMap<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it will resolve the issue completely because the next request might happened on different brokers.

@thetumbled thetumbled Aug 22, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All request for setting topic policy will be redirected to the owner broker of the partition-0, so we do not need to consider distributed situations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All request for setting topic policy will be redirected to the owner broker of the partition-0, so we do not need to consider distributed situations.

Could you share the code which routed the HTTP request to the "owner broker of the partition-0"? I want to double confirm it. Thanks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have such redirection logic in: org.apache.pulsar.broker.admin.impl.PersistentTopicsBase#preValidation

                        return getPartitionedTopicMetadataAsync(topicName, false, false)
                            .thenCompose(metadata -> {
                                if (metadata.partitions > 0) {
                                    return validateTopicOwnershipAsync(TopicName.get(topicName.toString()
                                    + PARTITIONED_TOPIC_SUFFIX + 0), authoritative);
                                } else {
                                    return validateTopicOwnershipAsync(topicName, authoritative);
                                }
                        });

@poorbarcode poorbarcode Aug 23, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two other concerns about the fix:

  • The problem still exists if the Topic Owner is transferred to another broker
  • Now the reading is correct. But the topic may have yet to apply the latest policy even if users received the response.

I'm also thinking about how we can implement this function simpler.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, even if all the read and update operations occur on the same broker, it remains crucial to serialize all such operations. Without serialization, the broker would still be incapable of ensuring that every modification is carried out on the most up-to-date data.

Update from v1 -> v2
Update from v1 -> v3

The changes from v2 will be missed.

Additionally, during the transfer of ownership, it is possible for ongoing updates to continue taking place on the old broker. Meanwhile, new requests directed to the new broker may still have an opportunity to update the data based on a potentially "dirty read."

@codelipenghui codelipenghui added the category/reliability The function does not work properly in certain specific environments or failures. e.g. data lost label Aug 21, 2023
@mattisonchao

Copy link
Copy Markdown
Member

From my understanding now. It should be a problem like a dirty reading. Right?

Since the design of the topic policy mechanism is the final consensus, it's a little bit hard for us to fix this kinda situation.

IMO, we can try to define a redirect logic here.

  1. non-partitioned topic policy redirect to owner broker.
  2. partitioned topic policy redirects to the partition-0's owner broker as a logical topic leader.

Afterwards, we move a distributed problem to a single machine which will be easier for us to handle it.

  • Defining a sync method to let the reader read the end of the topic at that moment is better than saving the operation future everywhere from my point of view.

@thetumbled

thetumbled commented Aug 22, 2023

Copy link
Copy Markdown
Member Author

From my understanding now. It should be a problem like a dirty reading. Right?

Since the design of the topic policy mechanism is the final consensus, it's a little bit hard for us to fix this kinda situation.

IMO, we can try to define a redirect logic here.

  1. non-partitioned topic policy redirect to owner broker.
  2. partitioned topic policy redirects to the partition-0's owner broker as a logical topic leader.

Yes, it is dirty reading problem.
Actually, we have such redirection logic in: org.apache.pulsar.broker.admin.impl.PersistentTopicsBase#preValidation

                        return getPartitionedTopicMetadataAsync(topicName, false, false)
                            .thenCompose(metadata -> {
                                if (metadata.partitions > 0) {
                                    return validateTopicOwnershipAsync(TopicName.get(topicName.toString()
                                    + PARTITIONED_TOPIC_SUFFIX + 0), authoritative);
                                } else {
                                    return validateTopicOwnershipAsync(topicName, authoritative);
                                }
                        });

So we do not need to consider distributed situations. @codelipenghui

  • Defining a sync method to let the reader read the end of the topic at that moment is better than saving the operation future everywhere from my point of view.

Currently, we have two places need to read the messages, the first one is in the backgroup thread triggered by reader.readNextAsync() asynchronously, the second one is here.

To achieve strong concurrency control, maybe we could make the whole setting logic running synchronously in same thread for each topic, which will make break change. But as you say, the design of the topic policy mechanism is the final consensus, we do not need such kind of strong guarantee, right?

IMO, i don't see any drawback of this solution in synchronous requests situations, which is the basic situation and enough for users. In synchronous requests situations, there will be only one result in the queue at most at a time, which will be removed before the corresponding request returns.

@github-actions

github-actions Bot commented Oct 6, 2023

Copy link
Copy Markdown

The pr had no activity for 30 days, mark with Stale label.

@github-actions github-actions Bot added the Stale label Oct 6, 2023
@Technoboy- Technoboy- modified the milestones: 3.2.0, 3.3.0 Dec 22, 2023
@coderzc coderzc removed this from the 3.3.0 milestone May 8, 2024
@coderzc coderzc added this to the 3.4.0 milestone May 8, 2024
@lhotari lhotari modified the milestones: 4.0.0, 4.1.0 Oct 11, 2024
@lhotari

lhotari commented Aug 13, 2025

Copy link
Copy Markdown
Member

The problem has been addressed with "PIP-428: Change TopicPoliciesService interface to fix consistency issues" and the implementation PR #24427.

One of the main differences compared to the approach in this PR is that existing TopicPolicies instances are never mutated. Instead, the existing instance is cloned and the mutation happens on the cloned instance. This ensures that threads that are referencing the instance won't get an inconsistent view on the instance. The mutation will be atomic from this perspective and become available via the cache. A similar pattern is used for namespace policy updates.

@lhotari

lhotari commented Aug 27, 2025

Copy link
Copy Markdown
Member

Closing since this has been addressed by PIP-428 implementation PR #24427

@lhotari lhotari closed this Aug 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category/reliability The function does not work properly in certain specific environments or failures. e.g. data lost doc-not-needed Your PR changes do not impact docs Stale type/bug The PR fixed a bug or issue reported a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants