Skip to content

Commit 2054f2b

Browse files
committed
Add basicPublish with ByteBuffer
1 parent fcaad28 commit 2054f2b

13 files changed

Lines changed: 697 additions & 30 deletions

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,7 @@
797797
<include>src/test/java/com/rabbitmq/client/test/ProtocolVersionMismatch.java</include>
798798
<include>src/test/java/com/rabbitmq/client/test/TestUtils.java</include>
799799
<include>src/test/java/com/rabbitmq/client/test/RpcTopologyRecordingTest.java</include>
800+
<include>src/test/java/com/rabbitmq/client/test/PublishWithByteBufferTest.java</include>
800801
</includes>
801802
<googleJavaFormat>
802803
<version>${google-java-format.version}</version>

src/main/java/com/rabbitmq/client/Channel.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.rabbitmq.client.AMQP.*;
2020

2121
import java.io.IOException;
22+
import java.nio.ByteBuffer;
2223
import java.util.Map;
2324
import java.util.concurrent.CompletableFuture;
2425
import java.util.concurrent.TimeoutException;
@@ -299,6 +300,59 @@ void basicPublish(String exchange, String routingKey, boolean mandatory, BasicPr
299300
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
300301
throws IOException;
301302

303+
/**
304+
* Publish a message with a {@link ByteBuffer} body for zero-copy transmission.
305+
*
306+
* <p>The buffer's content between its current position and its limit will be sent.
307+
* The caller must not modify the buffer after this call returns.
308+
*
309+
* @see #basicPublish(String, String, BasicProperties, byte[])
310+
* @param exchange the exchange to publish the message to
311+
* @param routingKey the routing key
312+
* @param props other properties for the message - routing headers etc
313+
* @param body the message body
314+
* @throws java.io.IOException if an error is encountered
315+
*/
316+
default void basicPublish(String exchange, String routingKey, BasicProperties props, ByteBuffer body) throws IOException {
317+
basicPublish(exchange, routingKey, false, false, props, body);
318+
}
319+
320+
/**
321+
* Publish a message with a {@link ByteBuffer} body for zero-copy transmission.
322+
*
323+
* @see #basicPublish(String, String, boolean, BasicProperties, byte[])
324+
* @param exchange the exchange to publish the message to
325+
* @param routingKey the routing key
326+
* @param mandatory true if the 'mandatory' flag is to be set
327+
* @param props other properties for the message - routing headers etc
328+
* @param body the message body
329+
* @throws java.io.IOException if an error is encountered
330+
*/
331+
default void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, ByteBuffer body)
332+
throws IOException {
333+
basicPublish(exchange, routingKey, mandatory, false, props, body);
334+
}
335+
336+
/**
337+
* Publish a message with a {@link ByteBuffer} body for zero-copy transmission.
338+
*
339+
* @see #basicPublish(String, String, boolean, boolean, BasicProperties, byte[])
340+
* @param exchange the exchange to publish the message to
341+
* @param routingKey the routing key
342+
* @param mandatory true if the 'mandatory' flag is to be set
343+
* @param immediate true if the 'immediate' flag is to be
344+
* set. Note that the RabbitMQ server does not support this flag.
345+
* @param props other properties for the message - routing headers etc
346+
* @param body the message body
347+
* @throws java.io.IOException if an error is encountered
348+
*/
349+
default void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, ByteBuffer body)
350+
throws IOException {
351+
byte[] bytes = new byte[body.remaining()];
352+
body.get(bytes);
353+
basicPublish(exchange, routingKey, mandatory, immediate, props, bytes);
354+
}
355+
302356
/**
303357
* Actively declare a non-autodelete, non-durable exchange with no extra arguments
304358
* @see com.rabbitmq.client.AMQP.Exchange.Declare

src/main/java/com/rabbitmq/client/impl/AMQCommand.java

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.ByteArrayOutputStream;
1919
import java.io.DataOutputStream;
2020
import java.io.IOException;
21+
import java.nio.ByteBuffer;
2122
import java.util.concurrent.locks.Lock;
2223
import java.util.concurrent.locks.ReentrantLock;
2324

@@ -86,6 +87,16 @@ public AMQCommand(com.rabbitmq.client.Method method, AMQContentHeader contentHea
8687
this.assembler = new CommandAssembler((Method) method, contentHeader, body, maxBodyLength);
8788
}
8889

90+
/**
91+
* Construct a command with a ByteBuffer body for zero-copy transmission.
92+
* @param method the wrapped method
93+
* @param contentHeader the wrapped content header
94+
* @param body the message body as a ByteBuffer
95+
*/
96+
public AMQCommand(com.rabbitmq.client.Method method, AMQContentHeader contentHeader, ByteBuffer body) {
97+
this.assembler = new CommandAssembler((Method) method, contentHeader, body);
98+
}
99+
89100
/** Public API - {@inheritDoc} */
90101
@Override
91102
public Method getMethod() {
@@ -122,29 +133,11 @@ public void transmit(AMQChannel channel) throws IOException {
122133
try {
123134
Method m = this.assembler.getMethod();
124135
if (m.hasContent()) {
125-
byte[] body = this.assembler.getContentBody();
126-
127-
Frame headerFrame = this.assembler.getContentHeader().toFrame(channelNumber, body.length);
128-
129-
int frameMax = connection.getFrameMax();
130-
boolean cappedFrameMax = frameMax > 0;
131-
int bodyPayloadMax = cappedFrameMax ? frameMax - EMPTY_FRAME_SIZE : body.length;
132-
133-
if (cappedFrameMax && headerFrame.size() > frameMax) {
134-
String msg = String.format("Content headers exceeded max frame size: %d > %d", headerFrame.size(), frameMax);
135-
throw new IllegalArgumentException(msg);
136-
}
137-
connection.writeFrame(m.toFrame(channelNumber));
138-
connection.writeFrame(headerFrame);
139-
140-
for (int offset = 0; offset < body.length; offset += bodyPayloadMax) {
141-
int remaining = body.length - offset;
142-
143-
int fragmentLength = (remaining < bodyPayloadMax) ? remaining
144-
: bodyPayloadMax;
145-
Frame frame = Frame.fromBodyFragment(channelNumber, body,
146-
offset, fragmentLength);
147-
connection.writeFrame(frame);
136+
ByteBuffer bbBody = this.assembler.getByteBufferBody();
137+
if (bbBody != null) {
138+
transmitWithByteBuffer(m, bbBody, channelNumber, connection);
139+
} else {
140+
transmitWithByteArray(m, this.assembler.getContentBody(), channelNumber, connection);
148141
}
149142
} else {
150143
connection.writeFrame(m.toFrame(channelNumber));
@@ -156,6 +149,52 @@ public void transmit(AMQChannel channel) throws IOException {
156149
connection.flush();
157150
}
158151

152+
private void transmitWithByteArray(Method m, byte[] body, int channelNumber, AMQConnection connection) throws IOException {
153+
Frame headerFrame = this.assembler.getContentHeader().toFrame(channelNumber, body.length);
154+
155+
int frameMax = connection.getFrameMax();
156+
boolean cappedFrameMax = frameMax > 0;
157+
int bodyPayloadMax = cappedFrameMax ? frameMax - EMPTY_FRAME_SIZE : body.length;
158+
159+
if (cappedFrameMax && headerFrame.size() > frameMax) {
160+
String msg = String.format("Content headers exceeded max frame size: %d > %d", headerFrame.size(), frameMax);
161+
throw new IllegalArgumentException(msg);
162+
}
163+
connection.writeFrame(m.toFrame(channelNumber));
164+
connection.writeFrame(headerFrame);
165+
166+
for (int offset = 0; offset < body.length; offset += bodyPayloadMax) {
167+
int remaining = body.length - offset;
168+
int fragmentLength = (remaining < bodyPayloadMax) ? remaining : bodyPayloadMax;
169+
Frame frame = Frame.fromBodyFragment(channelNumber, body, offset, fragmentLength);
170+
connection.writeFrame(frame);
171+
}
172+
}
173+
174+
private void transmitWithByteBuffer(Method m, ByteBuffer body, int channelNumber, AMQConnection connection) throws IOException {
175+
int bodySize = body.remaining();
176+
Frame headerFrame = this.assembler.getContentHeader().toFrame(channelNumber, bodySize);
177+
178+
int frameMax = connection.getFrameMax();
179+
boolean cappedFrameMax = frameMax > 0;
180+
int bodyPayloadMax = cappedFrameMax ? frameMax - EMPTY_FRAME_SIZE : bodySize;
181+
182+
if (cappedFrameMax && headerFrame.size() > frameMax) {
183+
String msg = String.format("Content headers exceeded max frame size: %d > %d", headerFrame.size(), frameMax);
184+
throw new IllegalArgumentException(msg);
185+
}
186+
connection.writeFrame(m.toFrame(channelNumber));
187+
connection.writeFrame(headerFrame);
188+
189+
int bodyPosition = body.position();
190+
for (int offset = 0; offset < bodySize; offset += bodyPayloadMax) {
191+
int remaining = bodySize - offset;
192+
int fragmentLength = (remaining < bodyPayloadMax) ? remaining : bodyPayloadMax;
193+
Frame frame = Frame.fromBodyFragment(channelNumber, body, bodyPosition + offset, fragmentLength);
194+
connection.writeFrame(frame);
195+
}
196+
}
197+
159198
@Override public String toString() {
160199
return toString(false);
161200
}

src/main/java/com/rabbitmq/client/impl/ChannelN.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.slf4j.LoggerFactory;
2929

3030
import java.io.IOException;
31+
import java.nio.ByteBuffer;
3132
import java.util.*;
3233
import java.util.concurrent.CompletableFuture;
3334
import java.util.concurrent.CopyOnWriteArrayList;
@@ -745,6 +746,62 @@ public void basicPublish(String exchange, String routingKey,
745746
metricsCollector.basicPublish(this, deliveryTag);
746747
}
747748

749+
/** Public API - {@inheritDoc} */
750+
@Override
751+
public void basicPublish(String exchange, String routingKey,
752+
BasicProperties props, ByteBuffer body)
753+
throws IOException
754+
{
755+
basicPublish(exchange, routingKey, false, false, props, body);
756+
}
757+
758+
/** Public API - {@inheritDoc} */
759+
@Override
760+
public void basicPublish(String exchange, String routingKey,
761+
boolean mandatory,
762+
BasicProperties props, ByteBuffer body)
763+
throws IOException
764+
{
765+
basicPublish(exchange, routingKey, mandatory, false, props, body);
766+
}
767+
768+
/** Public API - {@inheritDoc} */
769+
@Override
770+
public void basicPublish(String exchange, String routingKey,
771+
boolean mandatory, boolean immediate,
772+
BasicProperties props, ByteBuffer body)
773+
throws IOException
774+
{
775+
final long deliveryTag;
776+
if (nextPublishSeqNo > 0) {
777+
deliveryTag = getNextPublishSeqNo();
778+
unconfirmedSet.add(deliveryTag);
779+
nextPublishSeqNo++;
780+
} else {
781+
deliveryTag = 0;
782+
}
783+
if (props == null) {
784+
props = MessageProperties.MINIMAL_BASIC;
785+
}
786+
AMQP.Basic.Publish publish = new Basic.Publish.Builder()
787+
.exchange(exchange)
788+
.routingKey(routingKey)
789+
.mandatory(mandatory)
790+
.immediate(immediate)
791+
.build();
792+
try {
793+
ObservationCollector.PublishCall publishCall = properties -> {
794+
AMQCommand command = new AMQCommand(publish, properties, body);
795+
transmit(command);
796+
};
797+
observationCollector.publish(publishCall, publish, props, body, this.connectionInfo());
798+
} catch (IOException | AlreadyClosedException e) {
799+
metricsCollector.basicPublishFailure(this, e);
800+
throw e;
801+
}
802+
metricsCollector.basicPublish(this, deliveryTag);
803+
}
804+
748805
/** Public API - {@inheritDoc} */
749806
@Override
750807
public Exchange.DeclareOk exchangeDeclare(String exchange, String type,

src/main/java/com/rabbitmq/client/impl/CommandAssembler.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.rabbitmq.client.impl;
1717

1818
import java.io.IOException;
19+
import java.nio.ByteBuffer;
1920
import java.util.ArrayList;
2021
import java.util.List;
2122

@@ -50,6 +51,9 @@ private enum CAState {
5051
/** sum of the lengths of all fragments */
5152
private int bodyLength;
5253

54+
/** Zero-copy body buffer, mutually exclusive with bodyN usage on the outbound path */
55+
private final ByteBuffer byteBufferBody;
56+
5357
/** No bytes of content body not yet accumulated */
5458
private long remainingBodyBytes;
5559

@@ -61,6 +65,7 @@ public CommandAssembler(Method method, AMQContentHeader contentHeader, byte[] bo
6165
this.contentHeader = contentHeader;
6266
this.bodyN = new ArrayList<>(2);
6367
this.bodyLength = 0;
68+
this.byteBufferBody = null;
6469
this.remainingBodyBytes = 0;
6570
this.maxBodyLength = maxBodyLength;
6671
appendBodyFragment(body);
@@ -74,6 +79,24 @@ public CommandAssembler(Method method, AMQContentHeader contentHeader, byte[] bo
7479
}
7580
}
7681

82+
public CommandAssembler(Method method, AMQContentHeader contentHeader, ByteBuffer body) {
83+
this.method = method;
84+
this.contentHeader = contentHeader;
85+
this.bodyN = new ArrayList<>(0);
86+
this.bodyLength = body == null ? 0 : body.remaining();
87+
this.byteBufferBody = body;
88+
this.remainingBodyBytes = 0;
89+
this.maxBodyLength = Integer.MAX_VALUE;
90+
if (method == null) {
91+
this.state = CAState.EXPECTING_METHOD;
92+
} else if (contentHeader == null) {
93+
this.state = method.hasContent() ? CAState.EXPECTING_CONTENT_HEADER : CAState.COMPLETE;
94+
} else {
95+
this.remainingBodyBytes = contentHeader.getBodySize() - this.bodyLength;
96+
updateContentBodyState();
97+
}
98+
}
99+
77100
public synchronized Method getMethod() {
78101
return this.method;
79102
}
@@ -151,9 +174,19 @@ private byte[] coalesceContentBody() {
151174
}
152175

153176
public synchronized byte[] getContentBody() {
177+
if (byteBufferBody != null) {
178+
ByteBuffer dup = byteBufferBody.duplicate();
179+
byte[] result = new byte[dup.remaining()];
180+
dup.get(result);
181+
return result;
182+
}
154183
return coalesceContentBody();
155184
}
156185

186+
public synchronized ByteBuffer getByteBufferBody() {
187+
return this.byteBufferBody;
188+
}
189+
157190
private void appendBodyFragment(byte[] fragment) {
158191
if (fragment == null || fragment.length == 0) return;
159192
bodyN.add(fragment);

0 commit comments

Comments
 (0)