Conversation
lhotari
left a comment
There was a problem hiding this comment.
Thanks for the fixes and regression tests. The non-persistent chunk-count change looks consistent with the send loop, and the unencrypted batch ownership paths are improved. One oversized encrypted-batch path still needs the same ownership correction. I reviewed the code and tests but did not run the tests locally.
| // below; releasing it here as well would drop a live buffer back into the pool. | ||
| if (encryptedPayload != batchedMessageMetadataAndPayload) { | ||
| encryptedPayload.release(); | ||
| } |
There was a problem hiding this comment.
[BUG] Encrypted oversized batches still release the input buffer twice
Could you also transfer the container field to the buffer returned by encryption and cover successful encryption in the regression test? With a multi-message batch whose encrypted payload exceeds the limit, encryptMessage has already released its input before returning a different buffer:
pulsar/pulsar-client/src/main/java/org/apache/pulsar/client/impl/ProducerImpl.java
Lines 1012 to 1014 in 8352e29
The new inequality branch releases the encrypted output, but discard() still releases batchedMessageMetadataAndPayload, which points to that already-released input. This leaves the oversized-batch double release in place when encryption succeeds, both with and without compression. The current test mocks encryption as an identity operation, so it cannot exercise this case.
There was a problem hiding this comment.
Confirmed and fixed in 72bc03e. You are right that the inequality branch only settled who releases the encrypted output, not the stale field: encryptMessage releases its input and returns a different buffer on success, so batchedMessageMetadataAndPayload pointed at freed memory that discard() released again.
The field now follows whatever encryption returns, at both call sites, exactly as it already does for compression — which is the line I should have extended one call further. That makes the inequality check redundant, so it is gone and the branch is simpler than before. All four exits of encryptMessage are covered: encryption disabled and the failure-with-SEND path return the input, so the assignment is a no-op; success transfers ownership to the new buffer; the failure-with-FAIL path throws before the assignment, leaving the field on the unreleased input for resetPayloadAfterFailedPublishing.
Added testOversizedEncryptedBatchReleasesItsBuffersExactlyOnce, which mocks encryption the way the real one behaves (release the input, return a new buffer). The load-bearing assertion is on the pre-encryption buffer, since the encrypted output was already released exactly once before this change. It fails without the fix with "the pre-encryption batch payload was released again after encryption had already released it".
BatchMessageContainerImplTest passes 6/6, the pulsar-client module suite (494 tests) shows no new failures, RawBatchMessageContainerImplTest (7) and MessageChunkingTest (18) pass, and quickCheck is clean.
…ng-and-batch-release # Conflicts: # pulsar-client/src/test/java/org/apache/pulsar/client/impl/BatchMessageContainerImplTest.java
Motivation
Two independent buffer-ownership defects on the producer send path. They are unrelated in cause, so they
are kept as separate commits, but both end in the same place: a pooled buffer released more often than it
was retained, which hands a live buffer back to the allocator.
1. An oversized batch releases its payload twice.
BatchMessageContainerImplholds the batch payload inbatchedMessageMetadataAndPayload. The ownershiprule is set by the success path: once the payload is handed to a
ByteBufPair, that pair owns it, andclear()only nulls the field instead of releasing it.Both oversized branches of
createOpSendMsgbreak that rule by letting two owners release the same buffer.With compression and encryption off — the default —
getCompressedBatchMetadataAndPayload()returns thefield itself and
encryptMessagepasses it straight through, so all three references are the same buffer:This is reached by the ordinary path: batching on (the default) and a message over the maximum message
size. If the buffer has already been handed out again by the pool, the second release frees memory that is
still in use.
The same file has a related dangling reference.
applyCompressionreleases the buffer it is given andreturns a new one, but the field was never updated to follow it, so from that point on it pointed at freed
memory that
discard()would release again andresetPayloadAfterFailedPublishing()would write into. Thenon-client branch of
getCompressedBatchMetadataAndPayload(boolean)has the same shape and is reached byRawBatchMessageContainerImpl, which sets the compression type from the message being re-batched.2. Messages are "chunked" on non-persistent topics.
Chunking has been guarded against non-persistent topics since it was introduced (PIP-37, #4400), but only
at the point where chunking is performed, not where it is decided:
The send loop runs
totalChunkstimes regardless. On a non-persistent topic the guarded block is skippedwhile the loop still runs N times, so three things follow from that one skip:
chunkPayloadstays the whole payload, which is published N times;that deduplication cannot catch either;
retain()lives inside the skipped block, so the payload is handed to NByteBufPairswhile only one reference is held — N releases against a refcount of 1.
Nothing rejects the configuration: the builder only refuses chunking together with batching.
Modifications
Oversized batch release. Give each buffer exactly one owner, and drop the stale reference whenever
ownership moves. The single-message branch keeps
cmd.release()— the command does own the payload by then— and nulls the field before
discard()runs, mirroring whatclear()does on the success path. Themulti-message branch releases
encryptedPayloadonly when it really is a different buffer, which is thecase exactly when encryption produced one; otherwise
discard()is the single owner. Both compressionbranches now assign the new buffer back to the field.
Chunking on non-persistent topics. Move the topic-type check from where chunking is performed to where
it is decided, so the two agree: a non-persistent topic computes
totalChunks = 1and takes the ordinarysingle-message path. The check at the slicing site then becomes redundant and is reduced to
totalChunks > 1, matching the sibling site in the same method that has always tested only the chunk count. The repeatedTopicName.get(topic)parse on the send path is replaced by a field computed once in the constructor.Because the duplicate publishing and the refcount underflow are two symptoms of the same skipped block,
unifying the condition removes both; no separate accounting change is needed.
No wire format or API change. Applications sending large messages to a non-persistent topic with chunking
enabled now publish them once instead of N times; the message must still fit the maximum message size, as
it did before for every one of those N copies.
Verifying this change
This change added tests and can be verified as follows:
BatchMessageContainerImplTest.testOversizedBatchReleasesItsPayloadExactlyOnce: a recordingallocator hands out the batch buffer so the test can hold it without reflecting into private state; the
buffer is retained once to stand in for another holder, so an extra release is observable instead of
being swallowed by
ReferenceCountUtil.safeRelease. It fails before the change with a reference countof 0 where 1 is expected.
MessageChunkingTest.testLargeMessageOnNonPersistentTopicIsSentOnceWithoutChunking: a payloadlarger than
chunkMaxMessageSizeis sent to a non-persistent topic and must be delivered exactly once.It fails before the change with "the payload was published more than once on a non-persistent topic".
BatchMessageContainerImplTest(5),MessageChunkingTest(18) andRawBatchMessageContainerImplTest(7) pass; the
pulsar-clientmodule suite shows no new failures and./gradlew quickCheckis clean.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes
Chunking is now inert on non-persistent topics, where it never produced a message a consumer could
reassemble. The setting is still accepted.