fix: allocate rebatch buffer with uncompressed size (#26290) - #26292
fix: allocate rebatch buffer with uncompressed size (#26290)#26292waterWang wants to merge 1 commit into
Conversation
rebatchMessage allocates the output batch buffer using the compressed payload capacity, but then re-serializes the *uncompressed* single messages into it. When the compressed payload is much smaller than the uncompressed rebatched size, the buffer is undersized and SingleMessageMetadata.writeTo's zero-copy getBytes path (from _parsedBuffer) throws IndexOutOfBoundsException because Netty's getBytes does not auto-expand the destination. Fix: size the buffer from metadata.getUncompressedSize() so rebatching never writes past the initial capacity. Affects compressed batches on 4.0.x / 4.1.x (LTS); 4.2.0+ masks the issue via the LightProto 0.6.x ensureWritable upgrade, but the allocation is still incorrect there. Fixes apache#26290
|
Thanks for picking this up! Claude Fable 5 review below: Unfortunately
Verified with the reproducer below — it applies this PR's allocation change to the 4.0.13 sources and loads the class via the broker classpath: docker run -d --name pulsar-pr-repro -u 0 apachepulsar/pulsar:4.0.13 bash -c '
curl -sLo /tmp/RawBatchConverter.java https://raw.githubusercontent.com/apache/pulsar/v4.0.13/pulsar-broker/src/main/java/org/apache/pulsar/client/impl/RawBatchConverter.java
sed -i "s/buffer(payload.capacity())/buffer(metadata.getUncompressedSize())/" /tmp/RawBatchConverter.java
javac -nowarn -cp "/pulsar/lib/*" -d /tmp/patch /tmp/RawBatchConverter.java
PULSAR_CLASSPATH=/tmp/patch exec bin/pulsar standalone -nss -nfw'
docker exec pulsar-pr-repro bash -c 'until bin/pulsar-admin brokers healthcheck >/dev/null 2>&1; do sleep 3; done'
docker exec -i pulsar-pr-repro python3 - <<'EOF'
import pulsar
client = pulsar.Client("pulsar://localhost:6650")
producer = client.create_producer(
"persistent://public/default/compaction-mixed-repro",
batching_enabled=True,
batching_max_messages=1000,
batching_max_publish_delay_ms=1000,
block_if_queue_full=True,
)
# one batch: 200 keyless 1-byte payloads (compacted out -> 8B placeholders > 7B originals),
# then 800 keyed messages (kept -> zero-copy key writes)
for i in range(200):
producer.send_async(b"x", callback=lambda res, mid: None)
for i in range(800):
producer.send_async(b"payload-%d" % i, callback=lambda res, mid: None,
partition_key="%08d/%s" % (i, "k" * 120))
producer.flush()
client.close()
print("produced")
EOF
docker exec pulsar-pr-repro bin/pulsar-admin topics compact persistent://public/default/compaction-mixed-repro
sleep 5
docker exec pulsar-pr-repro bin/pulsar-admin topics compaction-status persistent://public/default/compaction-mixed-repro
# with this PR's allocation: "Error compacting: java.lang.IndexOutOfBoundsException: dstIndex: 4013"
# drop the curl/sed/javac lines (= stock 4.0.13): "Compaction was a success" |
Motivation
RawBatchConverter.rebatchMessageallocates the output batch buffer using the compressed payload capacity, but then re-serializes the uncompressed single messages into it. When the compressed payload is much smaller than the uncompressed rebatched size, the buffer is undersized andSingleMessageMetadata.writeTo()'s zero-copygetBytespath (from_parsedBuffer) throwsIndexOutOfBoundsExceptionbecause Netty'sgetBytesdoes not auto-expand the destination.Fix
Move
metadata.getUncompressedSize()before the buffer allocation and use it as the initial capacity instead ofpayload.capacity().Affected versions
SingleMessageMetadata.writeTo()callensureWritable()before the zero-copygetBytespath. This fix is still a defensive improvement for these versions.Verification
The reproducer from the issue (a topic with batched ZSTD-compressed messages and automatic compaction) can be used to verify the fix.
Fixes #26290