[fix][cli][branch-4.2] Don't pass unset pulsar-perf pending-message options to the producer - #26371
Merged
nodece merged 1 commit intoAug 19, 2026
Conversation
…ptions to the producer ### Motivation `pulsar-perf produce` can exhaust the client's direct memory against a slow broker, because nothing bounds its producer: - `PerformanceBaseArguments.memoryLimit` has no initializer and is passed on unconditionally, so the client memory limit is disabled unless `--memory-limit` is given. - `--max-outstanding` defaults to `DEFAULT_MAX_PENDING_MESSAGES`, which is 0, and that value was passed to `maxPendingMessages(...)` unconditionally. 0 is the client's "no message-count limit", so the pending-message queue is unbounded too. `ProducerImpl` only creates its semaphore `if (conf.getMaxPendingMessages() > 0)`, so with both bounds gone a producer that outruns its broker buffers without any limit. apache#15283 fixed the same shape for `--max-outstanding-across-partitions` by only applying the option when it was set. That one is the across-partitions budget, which is divided between partitions and ignored on a non-partitioned topic; `--max-outstanding` is the one that matters there. ### Modifications Both options become `Integer` with no default, so "the user did not pass this flag" is distinguishable from "the user asked for 0", and each is applied only when it was given. `createProducerBuilder` uses `newProducer(Schema.BYTES)` rather than the no-argument `newProducer()`. They build the same producer, except that the former carries the defaults the client applies when its memory limit is disabled (1000 / 50000, added in apache#15723 so that a producer without byte-based backpressure still has some) - which is what an unset option now falls back to. `ProducerConfigurationData.setMaxPendingMessagesAcrossPartitions` no longer rejects a value below `maxPendingMessages`. That check makes the two setters order-dependent, and it is what forced apache#15283 to guard on `> 0` rather than on "was it set": with a per-producer default of 1000 now in place, `pulsar-perf -p 500` would throw `IllegalArgumentException`. The relationship is enforced where it is used - `PartitionedProducerImpl` lowers the per-partition limit to its share of the budget when a budget is set. With no flags, `pulsar-perf produce` now throttles - it sets `blockIfQueueFull(true)` - instead of buffering without limit, on partitioned and non-partitioned topics alike. `-o 0` restores the old unbounded behaviour. Assisted-by: Claude Code (Opus 5)
10 tasks
Contributor
|
Thanks for the fix. One small edge case worth handling: |
nodece
approved these changes
Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #26340
Motivation
pulsar-perf producecan exhaust the client's direct memory against a slow broker (#26340), because onbranch-4.2nothing bounds its producer at all:PerformanceBaseArguments.memoryLimitis alongwith no initializer, andPerfClientUtilspasses it on unconditionally, so the client memory limit is disabled unless--memory-limitis given.--max-outstandingdefaults toProducerConfigurationData.DEFAULT_MAX_PENDING_MESSAGES, which is0, and that value was passed tomaxPendingMessages(...)unconditionally.0is the client's "no message-count limit", so the pending-message queue is unbounded too.With both bounds gone, a producer that outruns its broker buffers without limit.
ProducerImplonly creates its semaphoreif (conf.getMaxPendingMessages() > 0).#15283 already fixed exactly this shape for
--max-outstanding-across-partitions, by only applying the option when it was set. It could not fix--max-outstandingthe same way, because at the time there was nothing for an unset value to fall back to on the producer path this tool uses. There is:PulsarClient.newProducer(Schema)gives a producer the pre-PIP-120 defaults (1000 / 50000) when the client memory limit is disabled — #15723 added that precisely so a producer without byte-based backpressure still has some.pulsar-perfmisses it only because it calls the no-argumentnewProducer(), which never got that treatment.Note that #15283's fix is for partitioned topics: the across-partitions budget is what
PartitionedProducerImpldivides between partitions, and it is ignored on a non-partitioned topic.--max-outstandingis the one that matters there, and it is the one still being passed through.Modifications
pulsar-testclient:--max-outstandingand--max-outstanding-across-partitionsbecomeIntegerwith no default, so "the user did not pass this flag" is distinguishable from "the user asked for 0". Each is applied to the builder only when it was given, which is [fix][tools] Only apply maxPendingMessagesAcrossPartitions if it presents #15283's fix generalised to both options.createProducerBuilderusesnewProducer(Schema.BYTES)instead of the no-argumentnewProducer(). They are the same builder for abyte[]producer, except that this one carries the client's defaults for a client with the memory limit disabled — which is what an unset option now falls back to.pulsar-client:ProducerConfigurationData.setMaxPendingMessagesAcrossPartitionsno longer rejects a value belowmaxPendingMessages. That check makes the two setters order-dependent, and it is what forced [fix][tools] Only apply maxPendingMessagesAcrossPartitions if it presents #15283 to guard on> 0rather than on "was it set": with the per-producer default now in place at 1000,pulsar-perf -p 500would throwIllegalArgumentExceptionbefore this change. The relationship is enforced where it is used —PartitionedProducerImpllowers the per-partition limit to its share of the budget when a budget is set, and the budget means nothing on a non-partitioned topic.Effective behaviour for
pulsar-perf producewith no flags, on a client whose memory limit is disabled:maxPendingMessages = 1000,maxPendingMessagesAcrossPartitions = 50000,blockIfQueueFull = true— so it throttles instead of buffering without limit, on partitioned and non-partitioned topics alike. Passing-oor-pstill overrides, including-o 0for the old unbounded behaviour.Verifying this change
This change added tests and can be verified as follows:
PerformanceProducerTest#testPendingMessageLimitsAreLeftToTheClientWhenUnset— with no flags, the builder carries positive limits. Fails before this change, where they are 0.PerformanceProducerTest#testGivenPendingMessageLimitsAreApplied—-pon its own is applied and does not fail producer creation. Fails without thepulsar-clientchange.ProducerBuilderImplTest#testAcrossPartitionsLimitBelowMaxPendingMessagesIsAccepted— pins the relaxed validation.ProducerBuilderImplTest#testProducerBuilderImplWhenMaxPendingMessagesAcrossPartitionsPropertyIsInvalidErrorMessages— updated for the new message; a negative value is still rejected.PerformanceProducerTest#testMaxOutstanding(added by [fix][tools] Only apply maxPendingMessagesAcrossPartitions if it presents #15283) and#testBatchingDisabledstill pass.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
pulsar-perf producewithout-o/-pnow has a bounded pending-message queue where it previously had none. It blocks rather than failing, since it setsblockIfQueueFull(true), so a run against a slow broker is throttled instead of ending in anOutOfMemoryError. A run that relied on unbounded buffering can restore it with-o 0, or raise the bound with-o <n>.-oand-pno longer report a default in--help, because they no longer have one.maxPendingMessagesAcrossPartitionsno longer throwsIllegalArgumentExceptionwhen set belowmaxPendingMessages; it is accepted, and the per-partition limit is lowered to its share as before. Only code that relied on the exception is affected, and such a call could not previously succeed.Documentation
doc-requireddoc-not-neededdocdoc-completeThe option help text states that an unset value is left to the client.