This is an automated email from the ASF dual-hosted git repository.

RobertIndie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-client-go.git


The following commit(s) were added to refs/heads/master by this push:
     new ef2a6317 [improve][doc] Add chunking documentation for DLQ/RLQ and add 
integration tests (#1511)
ef2a6317 is described below

commit ef2a63176b874e175e95ae6fc22e5f27ecc093a5
Author: zhou zhuohan <[email protected]>
AuthorDate: Tue Jun 30 00:04:12 2026 +0800

    [improve][doc] Add chunking documentation for DLQ/RLQ and add integration 
tests (#1511)
    
    Master Issue: https://github.com/apache/pulsar/pull/21048
    
    Related Issue: https://github.com/apache/pulsar-client-go/pull/805
    
    ### Motivation
    
    In the Java SDK (PR https://github.com/apache/pulsar/pull/21048), the 
DLQ/RLQ producers are hardcoded with
    `enableBatching(false).enableChunking(true)` to ensure large chunked 
messages can be forwarded to DLQ/RLQ
    topics successfully.
    
    However, in the Go SDK, the DLQ/RLQ producers are created using 
`DLQPolicy.ProducerOptions`, which defaults
    to a zero-value `ProducerOptions{}` (i.e., `EnableChunking: false`, 
`DisableBatching: false`). This means
    that if a producer sends a chunked message larger than the broker's 
`maxMessageSize`, and the consumer is
    configured with DLQ/RLQ, the DLQ/RLQ producer will fail to forward the 
message because it cannot chunk it.
    
    We considered automatically enabling chunking in the DLQ/RLQ producers when 
the user has not explicitly
    configured `ProducerOptions`. However, since `EnableChunking` and 
`DisableBatching` are both `bool` fields
    with a zero value of `false`, there is no way to distinguish between:
    - The user intentionally setting them to `false` (meaning "I don't want 
chunking")
    - The user not configuring them at all (meaning "I didn't think about it")
    
    Automatically overriding these values would break backward compatibility 
for users who explicitly set
    `EnableChunking: false`. Therefore, instead of changing the default 
behavior, we add documentation to
    guide users to enable chunking in `DLQPolicy.ProducerOptions` when using 
chunked messages with DLQ/RLQ.
    
    ### Modifications
    
    - Added a note to the `EnableChunking` field documentation in 
`ProducerOptions` (`producer.go`) explaining
      that when chunking is enabled and the consumer uses DLQ/RLQ, it is 
recommended to also enable chunking
      in `DLQPolicy.ProducerOptions`.
    - Added a note to the `DLQPolicy.ProducerOptions` field documentation 
(`consumer.go`) explaining that if
      the messages being consumed were produced with chunking enabled, it is 
recommended to also enable chunking
      here. This helps in decoupled producer/consumer scenarios where the 
consumer developer may not be aware
      of the producer's chunking configuration.
    - Added `TestChunkReconsumeLater` integration test to verify chunked 
messages (5MB, exceeding broker
      maxMessageSize) can be sent to the retry topic via `ReconsumeLater` and 
routed to DLQ after exceeding
      max retries, when `DLQPolicy.ProducerOptions` has chunking enabled.
    - Added `TestChunkDLQWithNack` integration test to verify chunked messages 
(5MB) are routed to DLQ
      after exceeding max redelivery count via `Nack`, when 
`DLQPolicy.ProducerOptions` has chunking enabled.
    
    ### Verifying this change
    
    This change added tests and can be verified as follows:
    
    - Added `TestChunkReconsumeLater`: sends a 5MB chunked message, verifies it 
flows through retry topic
      via `ReconsumeLater` and eventually lands in DLQ after exceeding 
`MaxDeliveries`.
    - Added `TestChunkDLQWithNack`: sends a 5MB chunked message, verifies it is 
routed to DLQ after
      exceeding max redelivery count via `Nack`, with original properties 
preserved.
    
    Both tests use payloads larger than broker `maxMessageSize` (~1MB) to 
ensure that DLQ/RLQ producers
    **must** have chunking enabled to successfully forward messages. Without 
chunking enabled in
    `DLQPolicy.ProducerOptions`, these tests would timeout.
---
 pulsar/consumer.go              |   9 +-
 pulsar/message_chunking_test.go | 223 ++++++++++++++++++++++++++++++++++++++++
 pulsar/producer.go              |  12 +++
 3 files changed, 243 insertions(+), 1 deletion(-)

diff --git a/pulsar/consumer.go b/pulsar/consumer.go
index 9aeabb03..5cff870f 100644
--- a/pulsar/consumer.go
+++ b/pulsar/consumer.go
@@ -83,7 +83,14 @@ type DLQPolicy struct {
        // a topic.
        DeadLetterTopicProducerName string
 
-       // ProducerOptions is the producer options to produce messages to the 
DLQ and RLQ topic
+       // ProducerOptions is the producer options to produce messages to the 
DLQ and RLQ topic.
+       // If the messages being consumed were produced with chunking enabled, 
it is recommended to also enable
+       // chunking here so that large chunked messages can be forwarded to the 
DLQ/RLQ topic successfully.
+       // Example:
+       //   ProducerOptions: pulsar.ProducerOptions{
+       //       EnableChunking:  true,
+       //       DisableBatching: true,
+       //   }
        ProducerOptions ProducerOptions
 
        // RetryLetterTopic specifies the name of the topic where the retry 
messages will be sent.
diff --git a/pulsar/message_chunking_test.go b/pulsar/message_chunking_test.go
index 2d6462e7..546412f9 100644
--- a/pulsar/message_chunking_test.go
+++ b/pulsar/message_chunking_test.go
@@ -745,3 +745,226 @@ func TestResendChunkWithAckHoleMessages(t *testing.T) {
        cancel()
        assert.ErrorIs(t, err, context.DeadlineExceeded)
 }
+
+// TestChunkReconsumeLater tests that chunked messages can be sent to the 
retry topic via ReconsumeLater,
+// and are routed to the DLQ topic after exceeding the maximum number of 
retries.
+// Payload exceeds broker maxMessageSize, so RLQ/DLQ producers must enable 
chunking.
+func TestChunkReconsumeLater(t *testing.T) {
+       client, err := NewClient(ClientOptions{
+               URL: lookupURL,
+       })
+       assert.Nil(t, err)
+       defer client.Close()
+
+       topic := newTopicName()
+       subName := fmt.Sprintf("chunk-rlq-sub-%d", time.Now().Unix())
+       maxRedeliveries := 2
+
+       // Create producer with chunking enabled
+       producer, err := client.CreateProducer(ProducerOptions{
+               Topic:           topic,
+               DisableBatching: true,
+               EnableChunking:  true,
+       })
+       assert.NoError(t, err)
+       assert.NotNil(t, producer)
+       defer producer.Close()
+
+       // Create consumer with retry and DLQ enabled
+       // DLQPolicy.ProducerOptions enables chunking to ensure RLQ/DLQ 
producers can send large messages
+       consumer, err := client.Subscribe(ConsumerOptions{
+               Topic:                       topic,
+               SubscriptionName:            subName,
+               Type:                        Shared,
+               SubscriptionInitialPosition: SubscriptionPositionEarliest,
+               DLQ: &DLQPolicy{
+                       MaxDeliveries: uint32(maxRedeliveries),
+                       ProducerOptions: ProducerOptions{
+                               DisableBatching: true,
+                               EnableChunking:  true,
+                       },
+               },
+               RetryEnable:         true,
+               NackRedeliveryDelay: 1 * time.Second,
+       })
+       assert.NoError(t, err)
+       assert.NotNil(t, consumer)
+       defer consumer.Close()
+
+       // Send a chunked message larger than broker maxMessageSize (5MB, far 
exceeding the 1MB broker limit)
+       // If RLQ/DLQ producer does not enable chunking, sending will fail
+       content := createTestMessagePayload(5 * _brokerMaxMessageSize)
+       msgID, err := producer.Send(context.Background(), &ProducerMessage{
+               Payload: content,
+               Key:     "chunk-key",
+       })
+       assert.NoError(t, err)
+       assert.NotNil(t, msgID)
+
+       // First receive and ReconsumeLater
+       ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       msg, err := consumer.Receive(ctx)
+       cancel()
+       assert.NoError(t, err)
+       assert.Equal(t, content, msg.Payload())
+       assert.Equal(t, "chunk-key", msg.Key())
+       consumer.ReconsumeLater(msg, 1*time.Second)
+
+       // Second receive (from retry topic), ReconsumeLater again
+       // If RLQ producer does not enable chunking, the message won't be 
received here (send failure)
+       ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
+       msg, err = consumer.Receive(ctx)
+       cancel()
+       assert.NoError(t, err)
+       assert.Equal(t, content, msg.Payload())
+       consumer.ReconsumeLater(msg, 1*time.Second)
+
+       // Third receive (from retry topic), ReconsumeLater again, now exceeds 
maxRedeliveries, should route to DLQ
+       ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
+       msg, err = consumer.Receive(ctx)
+       cancel()
+       assert.NoError(t, err)
+       assert.Equal(t, content, msg.Payload())
+       consumer.ReconsumeLater(msg, 1*time.Second)
+
+       // Confirm no more messages on the original consumer
+       ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+       msg, err = consumer.Receive(ctx)
+       cancel()
+       assert.ErrorIs(t, err, context.DeadlineExceeded)
+       assert.Nil(t, msg)
+
+       // Consume from DLQ topic, verify the large chunked message is 
correctly routed to DLQ
+       // If DLQ producer does not enable chunking, the message won't be 
received here
+       dlqTopic := "persistent://public/default/" + topic + "-" + subName + 
"-DLQ"
+       dlqConsumer, err := client.Subscribe(ConsumerOptions{
+               Topic:                       dlqTopic,
+               SubscriptionName:            "dlq-sub",
+               SubscriptionInitialPosition: SubscriptionPositionEarliest,
+       })
+       assert.NoError(t, err)
+       defer dlqConsumer.Close()
+
+       ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
+       dlqMsg, err := dlqConsumer.Receive(ctx)
+       cancel()
+       assert.NoError(t, err)
+       assert.Equal(t, content, dlqMsg.Payload())
+       assert.Equal(t, "chunk-key", dlqMsg.Key())
+
+       // Verify DLQ message properties
+       assert.NotEmpty(t, dlqMsg.Properties()[SysPropertyRealTopic])
+       assert.NotEmpty(t, dlqMsg.Properties()[SysPropertyOriginMessageID])
+
+       assert.NoError(t, dlqConsumer.Ack(dlqMsg))
+
+       // No more messages on the DLQ topic
+       ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
+       dlqMsg, err = dlqConsumer.Receive(ctx)
+       cancel()
+       assert.ErrorIs(t, err, context.DeadlineExceeded)
+       assert.Nil(t, dlqMsg)
+}
+
+// TestChunkDLQWithNack tests that chunked messages trigger redelivery via 
Nack,
+// and are routed to the DLQ topic after exceeding the maximum redelivery 
count.
+// Uses a payload larger than broker maxMessageSize to ensure DLQ producer 
must enable chunking to send successfully.
+func TestChunkDLQWithNack(t *testing.T) {
+       client, err := NewClient(ClientOptions{
+               URL: lookupURL,
+       })
+       assert.Nil(t, err)
+       defer client.Close()
+
+       topic := newTopicName()
+       dlqTopic := newTopicName()
+       subName := "chunk-dlq-sub"
+       maxRedeliveries := uint32(2)
+
+       // Create DLQ consumer
+       dlqConsumer, err := client.Subscribe(ConsumerOptions{
+               Topic:            dlqTopic,
+               SubscriptionName: "dlq-verify",
+       })
+       assert.NoError(t, err)
+       defer dlqConsumer.Close()
+
+       // Create producer with chunking enabled
+       producer, err := client.CreateProducer(ProducerOptions{
+               Topic:           topic,
+               DisableBatching: true,
+               EnableChunking:  true,
+       })
+       assert.NoError(t, err)
+       assert.NotNil(t, producer)
+       defer producer.Close()
+
+       // Create consumer with DLQ configured
+       // DLQPolicy.ProducerOptions enables chunking to ensure DLQ producer 
can send large messages
+       consumer, err := client.Subscribe(ConsumerOptions{
+               Topic:            topic,
+               SubscriptionName: subName,
+               Type:             Shared,
+               DLQ: &DLQPolicy{
+                       MaxDeliveries:   maxRedeliveries,
+                       DeadLetterTopic: dlqTopic,
+                       ProducerOptions: ProducerOptions{
+                               DisableBatching: true,
+                               EnableChunking:  true,
+                       },
+               },
+               NackRedeliveryDelay: 1 * time.Second,
+       })
+       assert.NoError(t, err)
+       defer consumer.Close()
+
+       // Send a chunked message larger than broker maxMessageSize (5MB, far 
exceeding the 1MB broker limit)
+       // If DLQ producer does not enable chunking, sending will fail
+       content := createTestMessagePayload(5 * _brokerMaxMessageSize)
+       _, err = producer.Send(context.Background(), &ProducerMessage{
+               Payload: content,
+               Key:     "chunk-dlq-key",
+               Properties: map[string]string{
+                       "custom-prop": "custom-value",
+               },
+       })
+       assert.NoError(t, err)
+
+       // Receive and Nack the message maxRedeliveries times
+       for i := 0; i < int(maxRedeliveries); i++ {
+               ctx, cancel := context.WithTimeout(context.Background(), 
15*time.Second)
+               msg, err := consumer.Receive(ctx)
+               cancel()
+               assert.NoError(t, err)
+               assert.Equal(t, content, msg.Payload())
+               assert.Equal(t, "chunk-dlq-key", msg.Key())
+               consumer.Nack(msg)
+       }
+
+       // Message should be routed to DLQ
+       // If DLQ producer does not enable chunking, the large message cannot 
be sent and this will timeout
+       ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+       dlqMsg, err := dlqConsumer.Receive(ctx)
+       cancel()
+       assert.NoError(t, err)
+       assert.NotNil(t, dlqMsg)
+       assert.Equal(t, content, dlqMsg.Payload())
+       assert.Equal(t, "chunk-dlq-key", dlqMsg.Key())
+
+       // Verify original properties are preserved
+       assert.Equal(t, "custom-value", dlqMsg.Properties()["custom-prop"])
+
+       // Verify DLQ metadata properties
+       assert.NotEmpty(t, dlqMsg.Properties()[SysPropertyRealTopic])
+       assert.Contains(t, dlqMsg.Properties()[SysPropertyRealTopic], topic)
+       assert.NotEmpty(t, dlqMsg.Properties()[SysPropertyOriginMessageID])
+
+       assert.NoError(t, dlqConsumer.Ack(dlqMsg))
+
+       // No more messages on the original consumer
+       ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
+       msg, err := consumer.Receive(ctx)
+       cancel()
+       assert.ErrorIs(t, err, context.DeadlineExceeded)
+       assert.Nil(t, msg)
+}
diff --git a/pulsar/producer.go b/pulsar/producer.go
index c8b6b09c..e3ea7c13 100644
--- a/pulsar/producer.go
+++ b/pulsar/producer.go
@@ -198,6 +198,18 @@ type ProducerOptions struct {
        // EnableChunking controls whether automatic chunking of messages is 
enabled for the producer. By default, chunking
        // is disabled.
        // Chunking can not be enabled when batching is enabled.
+       //
+       // Note: If chunking is enabled and the consumer is configured with 
DLQ/RLQ (Dead Letter Queue / Retry Letter
+       // Queue), it is recommended to also enable chunking in the DLQ 
policy's ProducerOptions so that large chunked
+       // messages can be forwarded to the DLQ/RLQ topic successfully. Example:
+       //   DLQ: &pulsar.DLQPolicy{
+       //       MaxDeliveries:   3,
+       //       DeadLetterTopic: "my-dlq-topic",
+       //       ProducerOptions: pulsar.ProducerOptions{
+       //           EnableChunking:  true,
+       //           DisableBatching: true,
+       //       },
+       //   }
        EnableChunking bool
 
        // ChunkMaxMessageSize is the max size of single chunk payload.

Reply via email to