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

AndrewJSchofield pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new d766e485518 KAFKA-20686: Expand integration test suite for share group 
DLQ. [2/N] (#22671)
d766e485518 is described below

commit d766e4855184bfd72f0d894e8c6307564c24af30
Author: Sushant Mahajan <[email protected]>
AuthorDate: Fri Jun 26 21:40:09 2026 +0530

    KAFKA-20686: Expand integration test suite for share group DLQ. [2/N] 
(#22671)
    
    In this PR we have added integ tests for share group DLQ to include more
    scenarios:
    * gating DLQ (enable/disable)
    * copy record enabled when DLQ
    * mixed ack type responses from consumer (covers coalescing)
    * dynamic toggling of DLQ
    * multi partition DLQ (source and DLQ topic).
    
    Reviewers: Andrew Schofield <[email protected]>
---
 .../clients/consumer/ShareConsumerDLQTest.java     | 368 +++++++++++++++++++--
 1 file changed, 340 insertions(+), 28 deletions(-)

diff --git 
a/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerDLQTest.java
 
b/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerDLQTest.java
index e67d66b0d73..9abb85fc1b9 100644
--- 
a/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerDLQTest.java
+++ 
b/clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerDLQTest.java
@@ -20,6 +20,8 @@ import org.apache.kafka.clients.admin.Admin;
 import org.apache.kafka.clients.admin.Config;
 import org.apache.kafka.clients.admin.ConfigEntry;
 import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.producer.Producer;
+import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.config.ConfigResource;
 import org.apache.kafka.common.config.TopicConfig;
@@ -104,10 +106,10 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
         produceMessages(recordCount);
 
         // Reject every record using an EXPLICIT-mode share consumer.
-        rejectAllRecords(groupId, recordCount);
+        rejectRecords(groupId, recordCount);
 
         // Verify by reading from the DLQ topic, then assert the DLQ metrics.
-        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount));
+        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount), false);
         verifyDlqMetrics(groupId, recordCount);
     }
 
@@ -133,11 +135,11 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
         alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
 
         produceMessages(recordCount);
-        rejectAllRecords(groupId, recordCount);
+        rejectRecords(groupId, recordCount);
 
         // Verify the DLQ topic was auto-created (with DLQ enabled), received 
the records, and metrics fired.
         verifyDlqTopicCreated(dlqTopic);
-        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount));
+        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount), false);
         verifyDlqMetrics(groupId, recordCount);
     }
 
@@ -166,29 +168,278 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
         // Repeatedly release the records until their delivery count is 
exceeded and they are written to the DLQ.
         releaseRecordsUntilDlq(groupId, recordCount);
 
-        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount));
+        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount), false);
         verifyDlqMetrics(groupId, recordCount);
     }
 
-    // Consumes from the source topic in EXPLICIT acknowledgement mode and 
rejects every record.
-    private void rejectAllRecords(String groupId, int recordCount) {
+    /**
+     * Produces records and applies a mix of acknowledgement types so the DLQ 
is reached via more than one path:
+     * some records are rejected (DLQ via client reject), some are released on 
every delivery so they reach the
+     * DLQ once their delivery count is exceeded (release-based DLQ), and the 
rest are accepted (never DLQ'd).
+     * The actions are interleaved by offset so the DLQ'd offsets are 
non-contiguous and produce multiple
+     * separate DLQ writes to the same DLQ partition. The DLQ topic is created 
manually up front.
+     */
+    @ClusterTest
+    public void testMixedAcknowledgementTypesWrittenToDlq() throws Exception {
+        String groupId = "dlq-mixed-group";
+        // The broker's default share-group DLQ topic prefix is "dlq.", so the 
topic name must start with it.
+        String dlqTopic = "dlq.mixed";
+        int recordCount = 6;
+
+        alterShareAutoOffsetReset(groupId, "earliest");
+        // Low delivery count limit so the released records reach the DLQ 
(delivery count exceeded) quickly.
+        alterShareDeliveryCountLimit(groupId, "2");
+        createDlqTopic(dlqTopic);
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
+
+        produceMessages(recordCount);
+
+        // Assign each source offset an action, interleaved so the DLQ'd 
offsets are non-contiguous:
+        //   offset % 3 == 0 -> reject  (DLQ via client reject)
+        //   offset % 3 == 1 -> release (DLQ via delivery count exceeded - 
released on every delivery)
+        //   offset % 3 == 2 -> accept  (never DLQ'd)
+        Set<Long> rejectOffsets = new HashSet<>();
+        Set<Long> releaseOffsets = new HashSet<>();
+        for (long offset = 0; offset < recordCount; offset++) {
+            if (offset % 3 == 0) {
+                rejectOffsets.add(offset);
+            } else if (offset % 3 == 1) {
+                releaseOffsets.add(offset);
+            }
+        }
+        Set<Long> expectedDlqOffsets = new HashSet<>();
+        expectedDlqOffsets.addAll(rejectOffsets);
+        expectedDlqOffsets.addAll(releaseOffsets);
+
         try (ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer(
             groupId, Map.of(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, 
EXPLICIT))) {
             shareConsumer.subscribe(Set.of(tp.topic()));
-            int rejected = 0;
+            // Keep polling/acknowledging until every rejected and released 
record has reached the DLQ. Released
+            // records are released on every delivery, so they are redelivered 
until their delivery count is
+            // exceeded and they are archived to the DLQ.
             long deadlineMs = System.currentTimeMillis() + DEFAULT_MAX_WAIT_MS;
-            while (rejected < recordCount && System.currentTimeMillis() < 
deadlineMs) {
+            while (dlqMeterCount(METRIC_DLQ_RECORD_COUNT, groupId) < 
expectedDlqOffsets.size()
+                && System.currentTimeMillis() < deadlineMs) {
                 ConsumerRecords<byte[], byte[]> records = 
shareConsumer.poll(Duration.ofMillis(2000));
                 for (ConsumerRecord<byte[], byte[]> record : records) {
-                    shareConsumer.acknowledge(record, AcknowledgeType.REJECT);
-                    rejected++;
+                    long offset = record.offset();
+                    if (rejectOffsets.contains(offset)) {
+                        shareConsumer.acknowledge(record, 
AcknowledgeType.REJECT);
+                    } else if (releaseOffsets.contains(offset)) {
+                        shareConsumer.acknowledge(record, 
AcknowledgeType.RELEASE);
+                    } else {
+                        shareConsumer.acknowledge(record, 
AcknowledgeType.ACCEPT);
+                    }
                 }
                 if (records.count() > 0) {
                     shareConsumer.commitSync(Duration.ofMillis(10000));
                 }
             }
-            assertEquals(recordCount, rejected, "Expected to reject all 
produced records");
         }
+
+        // Exactly the rejected and released records should be on the DLQ; 
accepted records must not be.
+        verifyDlqTopicRecords(dlqTopic, groupId, expectedDlqOffsets, false);
+        verifyDlqMetrics(groupId, expectedDlqOffsets.size());
+    }
+
+    /**
+     * As {@link #testRejectedRecordsWrittenToDlqWithCopyRecordDisabled()}, 
but with record copy enabled, so
+     * each DLQ record must carry the original key and value (in addition to 
the context headers).
+     */
+    @ClusterTest
+    public void testRejectedRecordsWrittenToDlqWithCopyRecordEnabled() throws 
Exception {
+        String groupId = "dlq-copy-group";
+        String dlqTopic = "dlq.copy";
+        int recordCount = 5;
+
+        alterShareAutoOffsetReset(groupId, "earliest");
+        createDlqTopic(dlqTopic);
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
+        // Enable record copy so the original key/value are written onto the 
DLQ record.
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_COPY_RECORD_ENABLE_CONFIG, "true");
+
+        produceMessages(recordCount);
+        rejectRecords(groupId, recordCount);
+
+        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount), true);
+        verifyDlqMetrics(groupId, recordCount);
+    }
+
+    /**
+     * Rejects records from a multi-partition source topic and verifies they 
are routed to the correct DLQ
+     * partition. The destination partition is {@code sourcePartition % 
numDlqPartitions}; with a DLQ topic that
+     * has as many partitions as the source, each source partition maps to the 
DLQ partition of the same index.
+     */
+    @ClusterTest
+    public void testDlqRecordsRoutedToCorrectDlqPartition() throws Exception {
+        String groupId = "dlq-routing-group";
+        String sourceTopic = "dlq-routing-source";
+        String dlqTopic = "dlq.routing";
+        int partitions = 2;
+        int recordsPerPartition = 3;
+
+        createTopic(sourceTopic, partitions, 1);
+        alterShareAutoOffsetReset(groupId, "earliest");
+        createDlqTopic(dlqTopic, partitions);
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
+
+        // Produce the same number of records to each source partition.
+        for (int sourcePartition = 0; sourcePartition < partitions; 
sourcePartition++) {
+            produceTo(sourceTopic, sourcePartition, recordsPerPartition);
+        }
+
+        int total = partitions * recordsPerPartition;
+        rejectRecords(groupId, sourceTopic, total);
+        verifyDlqMetrics(groupId, total);
+
+        // Source partition p maps to DLQ partition (p % partitions) == p. 
Each DLQ partition should hold the
+        // records from the matching source partition (offsets 
0..recordsPerPartition-1).
+        for (int sourcePartition = 0; sourcePartition < partitions; 
sourcePartition++) {
+            List<ConsumerRecord<byte[], byte[]>> dlqRecords = 
readDlqPartition(dlqTopic, sourcePartition, recordsPerPartition);
+            assertEquals(recordsPerPartition, dlqRecords.size(),
+                "DLQ partition " + sourcePartition + " has an unexpected 
number of records");
+            Set<Long> offsets = new HashSet<>();
+            for (ConsumerRecord<byte[], byte[]> record : dlqRecords) {
+                assertEquals(groupId, headerValue(record, 
HEADER_DLQ_ERRORS_GROUP));
+                assertEquals(sourceTopic, headerValue(record, 
HEADER_DLQ_ERRORS_TOPIC));
+                assertEquals(Integer.toString(sourcePartition), 
headerValue(record, HEADER_DLQ_ERRORS_PARTITION),
+                    "Records on DLQ partition " + sourcePartition + " must 
originate from source partition " + sourcePartition);
+                
offsets.add(Long.parseLong(Objects.requireNonNull(headerValue(record, 
HEADER_DLQ_ERRORS_OFFSET))));
+            }
+            assertEquals(expectedSourceOffsets(recordsPerPartition), offsets);
+        }
+    }
+
+    /**
+     * Rejects a larger batch of records and verifies they all reach the DLQ. 
Exercises the produce coalescing /
+     * record-merge path at volume (many records DLQ'd to the same DLQ 
partition).
+     */
+    @ClusterTest
+    public void testManyRejectedRecordsAllWrittenToDlq() throws Exception {
+        String groupId = "dlq-scale-group";
+        String dlqTopic = "dlq.scale";
+        int recordCount = 500;
+
+        alterShareAutoOffsetReset(groupId, "earliest");
+        createDlqTopic(dlqTopic);
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
+
+        produceMessages(recordCount);
+        rejectRecords(groupId, recordCount);
+
+        verifyDlqTopicRecords(dlqTopic, groupId, 
expectedSourceOffsets(recordCount), false);
+        verifyDlqMetrics(groupId, recordCount);
+    }
+
+    /**
+     * Verifies the DLQ is NOT written when it is gated off, across the gating 
conditions:
+     *   (a) no DLQ topic configured for the group;
+     *   (b) the configured DLQ topic exists but is not DLQ-enabled;
+     *   (c) the configured DLQ topic does not exist and auto creation is 
disabled (the cluster default);
+     *   (d) the configured DLQ topic name does not match the broker's DLQ 
topic name prefix ("dlq.").
+     * In every case the rejected records are archived (the start offset 
advances) but no DLQ record is written.
+     */
+    @ClusterTest
+    public void testDlqNotTriggeredWhenGatedOff() throws Exception {
+        int recordCount = 3;
+
+        // (a) No DLQ topic configured for the group.
+        assertNoDlqWritten("dlq-gate-noname", "dlq-gate-source-a", 
recordCount, group -> { });
+
+        // (b) DLQ topic exists but is not DLQ-enabled.
+        assertNoDlqWritten("dlq-gate-disabled", "dlq-gate-source-b", 
recordCount, group -> {
+            createTopic("dlq.disabled");
+            alterShareGroupConfig(group, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, "dlq.disabled");
+        });
+
+        // (c) DLQ topic missing and auto creation disabled (broker default).
+        assertNoDlqWritten("dlq-gate-missing", "dlq-gate-source-c", 
recordCount, group ->
+            alterShareGroupConfig(group, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, "dlq.missing"));
+
+        // (d) DLQ topic is DLQ-enabled but its name violates the "dlq." 
prefix.
+        assertNoDlqWritten("dlq-gate-prefix", "dlq-gate-source-d", 
recordCount, group -> {
+            createDlqTopic("wrong.prefix");
+            alterShareGroupConfig(group, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, "wrong.prefix");
+        });
+    }
+
+    /**
+     * A single consumer rejects records while the DLQ is enabled (they are 
written to the DLQ), then the DLQ is
+     * turned off for the group by clearing its DLQ topic name, and the same 
consumer keeps rejecting more
+     * records. Verifies via the DLQ record-count metric that no further DLQ 
records are written after the DLQ
+     * is disabled.
+     */
+    @ClusterTest
+    public void testDlqStopsAfterDisablingForGroup() throws Exception {
+        String groupId = "dlq-toggle-group";
+        String dlqTopic = "dlq.toggle";
+        int firstBatch = 5;
+        int secondBatch = 5;
+
+        alterShareAutoOffsetReset(groupId, "earliest");
+        createDlqTopic(dlqTopic);
+        alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, dlqTopic);
+
+        try (ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer(
+            groupId, Map.of(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, 
EXPLICIT))) {
+            shareConsumer.subscribe(Set.of(tp.topic()));
+
+            // Phase 1: DLQ enabled - reject the first batch; they are written 
to the DLQ.
+            produceMessages(firstBatch);
+            rejectRecords(shareConsumer, firstBatch);
+            waitForCondition(() -> dlqMeterCount(METRIC_DLQ_RECORD_COUNT, 
groupId) == firstBatch,
+                DEFAULT_MAX_WAIT_MS, 200L,
+                () -> "First batch not written to DLQ, count was " + 
dlqMeterCount(METRIC_DLQ_RECORD_COUNT, groupId));
+
+            // Turn DLQ off for the group by clearing its DLQ topic name.
+            alterShareGroupConfig(groupId, 
GroupConfig.ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, "");
+
+            // Phase 2: DLQ disabled - the same consumer rejects more records; 
none should be DLQ'd.
+            produceMessages(secondBatch);
+            rejectRecords(shareConsumer, secondBatch);
+
+            // The second-batch records are archived (terminal) and not 
redelivered; confirming this also gives
+            // any (erroneous) DLQ write a chance to land before we assert.
+            for (int i = 0; i < 3; i++) {
+                assertEquals(0, 
shareConsumer.poll(Duration.ofMillis(2000)).count(),
+                    "Archived records must not be redelivered");
+            }
+        }
+
+        // Only the first batch (written while DLQ was enabled) should be on 
the DLQ.
+        assertEquals(firstBatch, dlqMeterCount(METRIC_DLQ_RECORD_COUNT, 
groupId),
+            "No DLQ records should be written after the DLQ is disabled for 
the group");
+    }
+
+    // Consumes from the base source topic in EXPLICIT acknowledgement mode 
and rejects `recordCount` records.
+    private void rejectRecords(String groupId, int recordCount) {
+        rejectRecords(groupId, tp.topic(), recordCount);
+    }
+
+    // Consumes from the given source topic in EXPLICIT acknowledgement mode 
and rejects `recordCount` records.
+    private void rejectRecords(String groupId, String topic, int recordCount) {
+        try (ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer(
+            groupId, Map.of(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, 
EXPLICIT))) {
+            shareConsumer.subscribe(Set.of(topic));
+            rejectRecords(shareConsumer, recordCount);
+        }
+    }
+
+    // Polls the given (already subscribed) consumer and rejects records until 
`count` have been rejected.
+    private void rejectRecords(ShareConsumer<byte[], byte[]> shareConsumer, 
int count) {
+        int rejected = 0;
+        long deadlineMs = System.currentTimeMillis() + DEFAULT_MAX_WAIT_MS;
+        while (rejected < count && System.currentTimeMillis() < deadlineMs) {
+            ConsumerRecords<byte[], byte[]> records = 
shareConsumer.poll(Duration.ofMillis(2000));
+            for (ConsumerRecord<byte[], byte[]> record : records) {
+                shareConsumer.acknowledge(record, AcknowledgeType.REJECT);
+                rejected++;
+            }
+            if (records.count() > 0) {
+                shareConsumer.commitSync(Duration.ofMillis(10000));
+            }
+        }
+        assertEquals(count, rejected, "Expected to reject the requested number 
of records");
     }
 
     // Repeatedly polls and releases every record until the delivery count 
limit is exceeded for all of them
@@ -211,6 +462,34 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
         }
     }
 
+    // Sets up a gating condition for a fresh group/source topic, rejects 
records, and asserts that although the
+    // records are archived (start offset advances) no DLQ record is ever 
written for the group. The setup
+    // callback receives the group id (java.util.function.Consumer is fully 
qualified to avoid clashing with the
+    // Kafka Consumer type in this package).
+    private void assertNoDlqWritten(String groupId, String sourceTopic, int 
recordCount,
+                                    java.util.function.Consumer<String> 
gateSetup) throws Exception {
+        createTopic(sourceTopic);
+        alterShareAutoOffsetReset(groupId, "earliest");
+        gateSetup.accept(groupId);
+
+        produceTo(sourceTopic, 0, recordCount);
+        rejectRecords(groupId, sourceTopic, recordCount);
+
+        // The rejected records are archived (terminal) so they are never 
redelivered. Confirming this also
+        // gives any DLQ attempt time to run (and, for the gated cases, to 
fail/short-circuit).
+        try (ShareConsumer<byte[], byte[]> shareConsumer = createShareConsumer(
+            groupId, Map.of(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, 
EXPLICIT))) {
+            shareConsumer.subscribe(Set.of(sourceTopic));
+            for (int i = 0; i < 3; i++) {
+                assertEquals(0, 
shareConsumer.poll(Duration.ofMillis(2000)).count(),
+                    "Archived records must not be redelivered for group: " + 
groupId);
+            }
+        }
+        // No DLQ record was written for this group (the per-group meter is 
never even registered).
+        assertEquals(-1L, dlqMeterCount(METRIC_DLQ_RECORD_COUNT, groupId),
+            "DLQ should not have been written for a gated-off group: " + 
groupId);
+    }
+
     private static Set<Long> expectedSourceOffsets(int recordCount) {
         Set<Long> offsets = new HashSet<>();
         for (long offset = 0; offset < recordCount; offset++) {
@@ -245,30 +524,32 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
     }
 
     /**
-     * Reads the DLQ topic and asserts it received exactly one record per 
expected source offset. Record copy
-     * is disabled, so each DLQ record carries only the context headers (no 
key/value).
+     * Reads the DLQ topic (single partition) and asserts it received exactly 
one record per expected source
+     * offset, each carrying the context headers for the base source 
topic-partition. When {@code copyEnabled}
+     * is true the DLQ records must also carry the original key/value; 
otherwise they carry headers only.
      *
      * @param dlqTopic              the DLQ topic to read from (single 
partition)
      * @param groupId               the share group the rejected records 
belonged to
      * @param expectedSourceOffsets the source offsets expected to have been 
written to the DLQ
+     * @param copyEnabled           whether record copy is enabled for the 
group
      */
-    private void verifyDlqTopicRecords(String dlqTopic, String groupId, 
Set<Long> expectedSourceOffsets) throws InterruptedException {
-        TopicPartition dlqTp = new TopicPartition(dlqTopic, 0);
-        List<ConsumerRecord<byte[], byte[]>> dlqRecords = new ArrayList<>();
-        try (Consumer<byte[], byte[]> consumer = cluster.consumer()) {
-            consumer.assign(List.of(dlqTp));
-            consumer.seekToBeginning(List.of(dlqTp));
-            waitForCondition(() -> {
-                
dlqRecords.addAll(consumer.poll(Duration.ofMillis(1000)).records(dlqTp));
-                return dlqRecords.size() >= expectedSourceOffsets.size();
-            }, DEFAULT_MAX_WAIT_MS, 500L, () -> "DLQ topic did not receive " + 
expectedSourceOffsets.size() + " records, got " + dlqRecords.size());
-        }
+    private void verifyDlqTopicRecords(String dlqTopic, String groupId, 
Set<Long> expectedSourceOffsets,
+                                       boolean copyEnabled) throws 
InterruptedException {
+        List<ConsumerRecord<byte[], byte[]>> dlqRecords = 
readDlqPartition(dlqTopic, 0, expectedSourceOffsets.size());
 
         assertEquals(expectedSourceOffsets.size(), dlqRecords.size(), 
"Unexpected number of records on the DLQ topic");
         Set<Long> actualSourceOffsets = new HashSet<>();
         for (ConsumerRecord<byte[], byte[]> record : dlqRecords) {
-            // Record copy is disabled, so only headers are written - the 
value (and key) are null.
-            assertNull(record.value(), "DLQ record value should be null when 
record copy is disabled");
+            if (copyEnabled) {
+                // produceMessages() produces records with key "key" and value 
"value".
+                assertEquals("key", new 
String(Objects.requireNonNull(record.key()), StandardCharsets.UTF_8),
+                    "DLQ record key should be copied when record copy is 
enabled");
+                assertEquals("value", new 
String(Objects.requireNonNull(record.value()), StandardCharsets.UTF_8),
+                    "DLQ record value should be copied when record copy is 
enabled");
+            } else {
+                // Record copy is disabled, so only headers are written - the 
value (and key) are null.
+                assertNull(record.value(), "DLQ record value should be null 
when record copy is disabled");
+            }
             assertEquals(groupId, headerValue(record, 
HEADER_DLQ_ERRORS_GROUP));
             assertEquals(tp.topic(), headerValue(record, 
HEADER_DLQ_ERRORS_TOPIC));
             assertEquals(Integer.toString(tp.partition()), headerValue(record, 
HEADER_DLQ_ERRORS_PARTITION));
@@ -277,16 +558,47 @@ public class ShareConsumerDLQTest extends 
ShareConsumerTestBase {
         assertEquals(expectedSourceOffsets, actualSourceOffsets, "DLQ records 
should cover every expected source offset");
     }
 
+    // Reads at least `expectedCount` records from a single DLQ 
topic-partition.
+    private List<ConsumerRecord<byte[], byte[]>> readDlqPartition(String 
dlqTopic, int partition, int expectedCount)
+            throws InterruptedException {
+        TopicPartition dlqTp = new TopicPartition(dlqTopic, partition);
+        List<ConsumerRecord<byte[], byte[]>> dlqRecords = new ArrayList<>();
+        try (Consumer<byte[], byte[]> consumer = cluster.consumer()) {
+            consumer.assign(List.of(dlqTp));
+            consumer.seekToBeginning(List.of(dlqTp));
+            waitForCondition(() -> {
+                
dlqRecords.addAll(consumer.poll(Duration.ofMillis(1000)).records(dlqTp));
+                return dlqRecords.size() >= expectedCount;
+            }, DEFAULT_MAX_WAIT_MS, 500L,
+                () -> dlqTp + " did not receive " + expectedCount + " records, 
got " + dlqRecords.size());
+        }
+        return dlqRecords;
+    }
+
     private void createDlqTopic(String topicName) {
+        createDlqTopic(topicName, 1);
+    }
+
+    private void createDlqTopic(String topicName, int numPartitions) {
         assertDoesNotThrow(() -> {
             try (Admin admin = createAdminClient()) {
-                NewTopic newTopic = new NewTopic(topicName, 1, (short) 1)
+                NewTopic newTopic = new NewTopic(topicName, numPartitions, 
(short) 1)
                     
.configs(Map.of(TopicConfig.ERRORS_DEADLETTERQUEUE_GROUP_ENABLE_CONFIG, 
"true"));
                 admin.createTopics(Set.of(newTopic)).all().get();
             }
         }, "Failed to create DLQ topic");
     }
 
+    // Produces `count` records (key "key", value "value") to a specific 
topic-partition.
+    private void produceTo(String topic, int partition, int count) {
+        try (Producer<byte[], byte[]> producer = createProducer()) {
+            for (int i = 0; i < count; i++) {
+                producer.send(new ProducerRecord<>(topic, partition, 
"key".getBytes(), "value".getBytes()));
+            }
+            producer.flush();
+        }
+    }
+
     private static String headerValue(ConsumerRecord<byte[], byte[]> record, 
String key) {
         Header header = record.headers().lastHeader(key);
         return header == null ? null : new String(header.value(), 
StandardCharsets.UTF_8);

Reply via email to