Skip to content

[fix][broker] Fix lookup permit leak when namespace policy reads fail - #26606

Merged
lhotari merged 4 commits into
apache:masterfrom
void-ptr974:fix/partition-metadata-lookup-cleanup
Oct 1, 2026
Merged

lhotari merged 4 commits into
apache:masterfrom
void-ptr974:fix/partition-metadata-lookup-cleanup

Conversation

@void-ptr974

@void-ptr974 void-ptr974 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Partition metadata lookup starts isAllowAutoTopicCreationAsync inside a callback without composing its future into the request chain. If the namespace policy read fails asynchronously, the broker sends no error response and never releases the acquired lookup permit. Repeated failures consume the permits shared by broker lookup and partition metadata lookup, leaving clients waiting for timeouts and subsequent requests rejected.

Modifications

  • Compose authorization, automatic topic creation policy checks, and metadata queries into one future chain.
  • Map failures and send responses within the stage that owns them, using handle while preserving the existing error mappings and response send paths.
  • Recycle TopicExistsInfo in the lookup callback's finally block. Release the lookup permit once in the terminal callback and log unexpected callback failures without attempting another response.
  • Add regression coverage for policy failures, delayed stage completion, lookup results and recycling, error mappings across both lookup paths, and cleanup-failure logging.

Verifying this change

  • Make sure that the change passes the CI checks.

This change added tests and can be verified as follows:

  • Policy failure tests cover immediately failed and delayed futures, both values of the client's metadata auto-creation flag, and three consecutive requests. They assert a single error response and restoration of the initial permit count.
  • Delayed authorization, policy, and metadata lookup tests verify stage ordering and that the permit remains held until the request completes.
  • Lookup result and error-mapping tests cover both lookup paths, partitioned/non-partitioned/missing topics, and wrapped failures. Cleanup tests verify recycling, permit restoration, no duplicate response, and one logged cleanup failure.
  • Local validation passed 46 scoped test cases (26 in ServerCnxTest and 20 in GetPartitionMetadataTest) and quickCheck, with retries disabled.
  • The four policy-failure regression cases fail on the pre-fix implementation due to missing error responses and pass with this fix.
./gradlew :pulsar-broker:test \
  --tests 'org.apache.pulsar.broker.service.ServerCnxTest.testPartitionMetadata*' \
  --tests 'org.apache.pulsar.broker.service.ServerCnxTest.handlePartitionMetadataRequestWithServiceNotReady' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testAutoCreatePartitionedTopic' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testAutoCreateNonPartitionedTopic' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testNamespaceNotExist' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testTenantNotExist' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testGetMetadataIfNotAllowedCreate' \
  --tests 'org.apache.pulsar.broker.admin.GetPartitionMetadataTest.testGetMetadataIfNotAllowedCreateOfNonPersistentTopic' \
  quickCheck -PtestRetryCount=0 -PtestMaxParallelForks=1 -PtestFailFast=false

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

Comment thread pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java Outdated
Comment thread pulsar-broker/src/main/java/org/apache/pulsar/broker/service/ServerCnx.java Outdated
Compose authorization, policy checks, and metadata queries while handling
business failures within their originating stages. Recycle topic existence
results locally, release lookup permits once after the request completes,
and log unexpected callback failures without attempting another response.

Cover delayed stage completion, existing-topic results, error mappings,
and cleanup observability. Validation: 46 scoped tests and quickCheck pass
with retries disabled. Policy failure regressions fail on the pre-fix
implementation and pass with this change.

Assisted-by: Codex
@void-ptr974

Copy link
Copy Markdown
Contributor Author

Updated in ace4156 to address both review comments.

Error and response handling now stays within each stage, lookup results are recycled locally, and permit release remains centralized.

Added tests for delayed authorization/policy/lookup completion, result recycling, and error mappings across both lookup paths. Local validation passed 46 scoped test cases and quickCheck, with retries disabled. The four policy-failure regression cases fail on the pre-fix implementation due to missing error responses and pass with the fix.

Retain both PulsarClientException and Schema imports in ServerCnxTest.

Assisted-by: Codex

@Denovo1998 Denovo1998 left a comment

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.

LGTM!

@lhotari lhotari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks for tracking down this permit leak and for the careful, stage-by-stage restructuring.

I traced the new chain in handlePartitionMetadataRequest: the permit is acquired once and released exactly once, in the terminal whenComplete, on every completion path (success, unauthorized, authorization failure, policy read failure, existence check or metadata lookup failure, and a synchronous throw from any stage via supplySafely). A throw while writing a response or recycling TopicExistsInfo propagates to that terminal stage, which releases once and logs without sending a second response. The old leak was real: the isAllowAutoTopicCreationAsync future was created inside a thenApply callback and never composed into the chain, so its failure was dropped and the permit was never released. Releasing after the response has been submitted and the result recycled, rather than before, is also the safer order. One small behaviour change worth noting in the description: a synchronous throw from the policy read now answers MetadataError rather than AuthorizationError.

The earlier review points (per-stage error mapping and making a recycle() failure visible) are addressed. The error mappings match the previous ones: unsafeGetPartitionedTopicMetadataAsync already unwraps the CompletionException, so the extra unwrap here does not change which error code is sent.

One optional follow-up on the tests is in the inline comment. Separately, and pre-existing (not for this PR): internalHandleGetTopicsOfNamespace has the same shape, where the listSizeHolder.getSizeAsync().thenAccept(...) future is not composed into anything that releases the lookup permit, so a failure of that future would strand the permit. It may be worth a look in a follow-up.

@lhotari
lhotari merged commit 5be997a into apache:master Oct 1, 2026
43 checks passed
@lhotari lhotari added this to the 5.0.0 milestone Oct 1, 2026
ascentstream-bot pushed a commit to ascentstream/pulsar that referenced this pull request Oct 1, 2026
ascentstream-bot pushed a commit to ascentstream/pulsar that referenced this pull request Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants