|
14 | 14 | import com.azure.core.amqp.implementation.handler.SendLinkHandler; |
15 | 15 | import com.azure.core.util.AsyncCloseable; |
16 | 16 | import com.azure.core.util.CoreUtils; |
| 17 | +import com.azure.core.util.FluxUtil; |
17 | 18 | import com.azure.core.util.logging.ClientLogger; |
18 | 19 | import org.apache.qpid.proton.Proton; |
19 | 20 | import org.apache.qpid.proton.amqp.Binary; |
|
26 | 27 | import org.apache.qpid.proton.amqp.transaction.Declared; |
27 | 28 | import org.apache.qpid.proton.amqp.transport.DeliveryState; |
28 | 29 | import org.apache.qpid.proton.amqp.transport.ErrorCondition; |
| 30 | +import org.apache.qpid.proton.codec.CompositeReadableBuffer; |
| 31 | +import org.apache.qpid.proton.codec.ReadableBuffer; |
29 | 32 | import org.apache.qpid.proton.engine.Delivery; |
30 | 33 | import org.apache.qpid.proton.engine.EndpointState; |
31 | 34 | import org.apache.qpid.proton.engine.Sender; |
|
44 | 47 | import java.nio.BufferOverflowException; |
45 | 48 | import java.time.Duration; |
46 | 49 | import java.time.Instant; |
| 50 | +import java.util.Arrays; |
47 | 51 | import java.util.Comparator; |
48 | 52 | import java.util.List; |
49 | 53 | import java.util.Locale; |
@@ -265,62 +269,103 @@ public Mono<Void> send(List<Message> messageBatch, DeliveryState deliveryState) |
265 | 269 |
|
266 | 270 | return getLinkSize() |
267 | 271 | .flatMap(maxMessageSize -> { |
268 | | - final Message firstMessage = messageBatch.get(0); |
| 272 | + int totalEncodedSize = 0; |
| 273 | + final CompositeReadableBuffer buffer = new CompositeReadableBuffer(); |
| 274 | + |
| 275 | + final byte[] envelopBytes = batchEnvelopBytes(messageBatch.get(0), maxMessageSize); |
| 276 | + if (envelopBytes.length > 0) { |
| 277 | + totalEncodedSize += envelopBytes.length; |
| 278 | + if (totalEncodedSize > maxMessageSize) { |
| 279 | + return batchBufferOverflowError(maxMessageSize); |
| 280 | + } |
| 281 | + buffer.append(envelopBytes); |
| 282 | + } |
269 | 283 |
|
270 | | - // proton-j doesn't support multiple dataSections to be part of AmqpMessage |
271 | | - // here's the alternate approach provided by them: https://github.com/apache/qpid-proton/pull/54 |
272 | | - final Message batchMessage = Proton.message(); |
273 | | - batchMessage.setMessageAnnotations(firstMessage.getMessageAnnotations()); |
| 284 | + for (final Message message : messageBatch) { |
| 285 | + final byte[] sectionBytes = batchBinaryDataSectionBytes(message, maxMessageSize); |
| 286 | + if (sectionBytes.length > 0) { |
| 287 | + totalEncodedSize += sectionBytes.length; |
| 288 | + if (totalEncodedSize > maxMessageSize) { |
| 289 | + return batchBufferOverflowError(maxMessageSize); |
| 290 | + } |
| 291 | + buffer.append(sectionBytes); |
| 292 | + } else { |
| 293 | + logger.info("Ignoring the empty message org.apache.qpid.proton.message.message@{} in the batch.", |
| 294 | + Integer.toHexString(System.identityHashCode(message))); |
| 295 | + } |
| 296 | + } |
274 | 297 |
|
275 | | - // Set partition identifier properties of the first message on batch message |
276 | | - if ((firstMessage.getMessageId() instanceof String) |
277 | | - && !CoreUtils.isNullOrEmpty((String) firstMessage.getMessageId())) { |
| 298 | + return send(buffer, AmqpConstants.AMQP_BATCH_MESSAGE_FORMAT, deliveryState); |
| 299 | + }).then(); |
| 300 | + } |
278 | 301 |
|
279 | | - batchMessage.setMessageId(firstMessage.getMessageId()); |
280 | | - } |
| 302 | + private byte[] batchEnvelopBytes(Message envelopMessage, int maxMessageSize) { |
| 303 | + // Proton-j doesn't support multiple dataSections to be part of AmqpMessage. |
| 304 | + // Here's the alternate approach provided: https://github.com/apache/qpid-proton/pull/54 |
| 305 | + final Message message = Proton.message(); |
| 306 | + message.setMessageAnnotations(envelopMessage.getMessageAnnotations()); |
281 | 307 |
|
282 | | - if (!CoreUtils.isNullOrEmpty(firstMessage.getGroupId())) { |
283 | | - batchMessage.setGroupId(firstMessage.getGroupId()); |
284 | | - } |
| 308 | + // Set partition identifier properties of the first message on batch message |
| 309 | + if ((envelopMessage.getMessageId() instanceof String) |
| 310 | + && !CoreUtils.isNullOrEmpty((String) envelopMessage.getMessageId())) { |
285 | 311 |
|
286 | | - final int maxMessageSizeTemp = maxMessageSize; |
287 | | - |
288 | | - final byte[] bytes = new byte[maxMessageSizeTemp]; |
289 | | - int encodedSize = batchMessage.encode(bytes, 0, maxMessageSizeTemp); |
290 | | - int byteArrayOffset = encodedSize; |
291 | | - |
292 | | - for (final Message amqpMessage : messageBatch) { |
293 | | - final Message messageWrappedByData = Proton.message(); |
294 | | - |
295 | | - int payloadSize = messageSerializer.getSize(amqpMessage); |
296 | | - int allocationSize = |
297 | | - Math.min(payloadSize + MAX_AMQP_HEADER_SIZE_BYTES, maxMessageSizeTemp); |
298 | | - |
299 | | - byte[] messageBytes = new byte[allocationSize]; |
300 | | - int messageSizeBytes = amqpMessage.encode(messageBytes, 0, allocationSize); |
301 | | - messageWrappedByData.setBody(new Data(new Binary(messageBytes, 0, messageSizeBytes))); |
302 | | - |
303 | | - try { |
304 | | - encodedSize = |
305 | | - messageWrappedByData |
306 | | - .encode(bytes, byteArrayOffset, maxMessageSizeTemp - byteArrayOffset - 1); |
307 | | - } catch (BufferOverflowException exception) { |
308 | | - final String message = |
309 | | - String.format(Locale.US, |
310 | | - "Size of the payload exceeded maximum message size: %s kb", |
311 | | - maxMessageSizeTemp / 1024); |
312 | | - final AmqpException error = new AmqpException(false, |
313 | | - AmqpErrorCondition.LINK_PAYLOAD_SIZE_EXCEEDED, message, exception, |
314 | | - handler.getErrorContext(sender)); |
315 | | - |
316 | | - return Mono.error(error); |
317 | | - } |
| 312 | + message.setMessageId(envelopMessage.getMessageId()); |
| 313 | + } |
318 | 314 |
|
319 | | - byteArrayOffset = byteArrayOffset + encodedSize; |
320 | | - } |
| 315 | + if (!CoreUtils.isNullOrEmpty(envelopMessage.getGroupId())) { |
| 316 | + message.setGroupId(envelopMessage.getGroupId()); |
| 317 | + } |
321 | 318 |
|
322 | | - return send(bytes, byteArrayOffset, AmqpConstants.AMQP_BATCH_MESSAGE_FORMAT, deliveryState); |
323 | | - }).then(); |
| 319 | + final int size = messageSerializer.getSize(message); |
| 320 | + final int allocationSize = Math.min(size + MAX_AMQP_HEADER_SIZE_BYTES, maxMessageSize); |
| 321 | + final byte[] encodedBytes = new byte[allocationSize]; |
| 322 | + final int encodedSize = message.encode(encodedBytes, 0, allocationSize); |
| 323 | + // This copyOf copying is just few bytes for envelop. |
| 324 | + return Arrays.copyOf(encodedBytes, encodedSize); |
| 325 | + } |
| 326 | + |
| 327 | + private byte[] batchBinaryDataSectionBytes(Message sectionMessage, int maxMessageSize) { |
| 328 | + final int size = messageSerializer.getSize(sectionMessage); |
| 329 | + final int allocationSize = Math.min(size + MAX_AMQP_HEADER_SIZE_BYTES, maxMessageSize); |
| 330 | + final byte[] encodedBytes = new byte[allocationSize]; |
| 331 | + final int encodedSize = sectionMessage.encode(encodedBytes, 0, allocationSize); |
| 332 | + |
| 333 | + final Message message = Proton.message(); |
| 334 | + final Data binaryData = new Data(new Binary(encodedBytes, 0, encodedSize)); |
| 335 | + message.setBody(binaryData); |
| 336 | + final int binaryRawSize = binaryData.getValue().getLength(); |
| 337 | + // Precompute the "amqp:data:binary" encoded size - |
| 338 | + final int binaryEncodedSize = binaryEncodedSize(binaryRawSize); |
| 339 | + // ^ this pre-computation avoids allocating byte[] 'arr1' of estimated encoded size (to pass to message.encode(arr1,)) |
| 340 | + // and a second allocation of byte[] 'arr2' with exact encoded size (returned from message.encode(arr1,)) then |
| 341 | + // copying encoded size bytes from 'arr1' to 'arr2'. Skipping extra allocations and CPU cycles for copying. |
| 342 | + final byte[] binaryEncodedBytes = new byte[binaryEncodedSize]; |
| 343 | + message.encode(binaryEncodedBytes, 0, binaryEncodedSize); |
| 344 | + return binaryEncodedBytes; |
| 345 | + } |
| 346 | + |
| 347 | + private Mono<Void> batchBufferOverflowError(int maxMessageSize) { |
| 348 | + return FluxUtil.monoError(logger, new AmqpException(false, AmqpErrorCondition.LINK_PAYLOAD_SIZE_EXCEEDED, |
| 349 | + String.format(Locale.US, "Size of the payload exceeded maximum message size: %s kb", maxMessageSize / 1024), |
| 350 | + new BufferOverflowException(), handler.getErrorContext(sender))); |
| 351 | + } |
| 352 | + |
| 353 | + /** |
| 354 | + * Compute the encoded size when encoding a binary data of given size per Amqp 1.0 spec "amqp:data:binary" format. |
| 355 | + * |
| 356 | + * @param binaryRawSize the length of the binary data. |
| 357 | + * @return the encoded size. |
| 358 | + */ |
| 359 | + private int binaryEncodedSize(int binaryRawSize) { |
| 360 | + if (binaryRawSize <= 255) { |
| 361 | + // [0x00,0x53,0x75,0xa0,{byte(Data.Binary.Length)},{Data.Binary.bytes}] |
| 362 | + // The AMQP 1.0 spec format ^ for amqp:data:binary when the raw bytes length is <= 255. |
| 363 | + return 5 + binaryRawSize; |
| 364 | + } else { |
| 365 | + // [0x00,0x53,0x75,0xb0,{int(Data.Binary.Length)},{Data.Binary.bytes}] |
| 366 | + // The AMQP 1.0 spec format ^ for amqp:data:binary when the raw bytes length is > 255. |
| 367 | + return 8 + binaryRawSize; |
| 368 | + } |
324 | 369 | } |
325 | 370 |
|
326 | 371 | @Override |
@@ -457,16 +502,24 @@ Mono<Void> isClosed() { |
457 | 502 |
|
458 | 503 | @Override |
459 | 504 | public Mono<DeliveryState> send(byte[] bytes, int arrayOffset, int messageFormat, DeliveryState deliveryState) { |
460 | | - final Flux<EndpointState> activeEndpointFlux = RetryUtil.withRetry( |
461 | | - handler.getEndpointStates().takeUntil(state -> state == EndpointState.ACTIVE), retryOptions, |
462 | | - activeTimeoutMessage); |
463 | | - |
464 | | - return activeEndpointFlux.then(Mono.create(sink -> { |
| 505 | + return onEndpointActive().then(Mono.create(sink -> { |
465 | 506 | sendWork(new RetriableWorkItem(bytes, arrayOffset, messageFormat, sink, retryOptions.getTryTimeout(), |
466 | 507 | deliveryState, metricsProvider)); |
467 | 508 | })); |
468 | 509 | } |
469 | 510 |
|
| 511 | + Mono<DeliveryState> send(ReadableBuffer buffer, int messageFormat, DeliveryState deliveryState) { |
| 512 | + return onEndpointActive().then(Mono.create(sink -> { |
| 513 | + sendWork(new RetriableWorkItem(buffer, messageFormat, sink, retryOptions.getTryTimeout(), |
| 514 | + deliveryState, metricsProvider)); |
| 515 | + })); |
| 516 | + } |
| 517 | + |
| 518 | + private Flux<EndpointState> onEndpointActive() { |
| 519 | + return RetryUtil.withRetry(handler.getEndpointStates().takeUntil(state -> state == EndpointState.ACTIVE), |
| 520 | + retryOptions, activeTimeoutMessage); |
| 521 | + } |
| 522 | + |
470 | 523 | /** |
471 | 524 | * Add the work item in pending send to be processed on {@link ReactorDispatcher} thread. |
472 | 525 | * |
@@ -536,10 +589,7 @@ private void processSendWork() { |
536 | 589 | if (workItem.isDeliveryStateProvided()) { |
537 | 590 | delivery.disposition(workItem.getDeliveryState()); |
538 | 591 | } |
539 | | - sentMsgSize = sender.send(workItem.getMessage(), 0, workItem.getEncodedMessageSize()); |
540 | | - assert sentMsgSize == workItem.getEncodedMessageSize() |
541 | | - : "Contract of the ProtonJ library for Sender. Send API changed"; |
542 | | - |
| 592 | + workItem.send(sender); |
543 | 593 | linkAdvance = sender.advance(); |
544 | 594 | } catch (Exception exception) { |
545 | 595 | sendException = exception; |
|
0 commit comments