apoorvmittal10 commented on code in PR #23360:
URL: https://github.com/apache/kafka/pull/23360#discussion_r3958014502


##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.
+ */
+public class ShareGroupDLQRecordHelper {
+
+    /**
+     * In most cases we expect the records getting DLQ'ed will be single 
offsets and
+     * not complete batches. Hence, using a large upper limit while reading 
from the log
+     * would be fruitless in most cases. Therefore, the value of 1 MB has been 
chosen
+     * for the DLQ-related log reads.
+     */
+    public static final int DLQ_MAX_FETCH_BYTES = 1024 * 1024;

Review Comment:
   Seems being used only in this class, shall we make it private?



##########
clients/clients-integration-tests/src/test/java/org/apache/kafka/clients/consumer/ShareConsumerDLQTest.java:
##########
@@ -583,6 +583,8 @@ public void 
testDlqRespectsDlqTopicMaxMessageBytesNotEqToSourceTopic() throws Ex
             }, DEFAULT_MAX_WAIT_MS, 100L, () -> "Raised max.message.bytes did 
not propagate on the DLQ topic");
         }
 
+        waitForDlqTopicConfigRefresh();

Review Comment:
   Comment here would be good regarding the significance of same.



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQValidator.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.config.ConfigException;
+
+import java.util.Optional;
+
+/**
+ * Shared validation logic for DLQ managers ({@link ShareGroupDLQStateManager} 
and
+ * {@code K2ShareGroupDLQManager}).
+ */
+public final class ShareGroupDLQValidator {
+
+    private ShareGroupDLQValidator() {}
+
+    /**
+     * Validates the fields of a {@link ShareGroupDLQRecordParameter}.
+     *
+     * @throws IllegalArgumentException if any field is invalid
+     */
+    public static void validateParam(ShareGroupDLQRecordParameter param) {
+        String prefix = "DLQ records parameters";
+        if (param == null) {
+            throw new IllegalArgumentException(prefix + " cannot be null.");
+        }
+        if (param.groupId() == null || param.groupId().isEmpty()) {
+            throw new IllegalArgumentException(prefix + " group cannot be null 
or empty.");
+        }
+        if (param.topicIdPartition() == null) {
+            throw new IllegalArgumentException(prefix + " topic/partition data 
cannot be null or empty.");
+        }
+        if (param.topicIdPartition().topicId() == null) {
+            throw new IllegalArgumentException(prefix + " topic id data cannot 
be null or empty.");
+        }
+        if (param.topicIdPartition().partition() < 0) {
+            throw new IllegalArgumentException(prefix + " partition cannot be 
negative.");
+        }
+        if (param.firstOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " first offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < param.firstOffset()) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be less than first offset.");
+        }
+    }
+
+    /**
+     * Validates DLQ topic configuration. Checks that the topic name does not 
start with {@code __},
+     * that DLQ is enabled on the topic (if it exists), and that the topic 
name complies with the
+     * configured prefix.
+     *
+     * <p>Callers are responsible for checking that the topic name is present 
in the config (non-empty)
+     * before calling this method, and for any implementation-specific checks 
(e.g., K1 auto-create).
+     *
+     * @param groupId           the share group ID, for error messages
+     * @param userTopicName     the raw DLQ topic name from config (without 
tenant prefix)
+     * @param resolvedTopicName the topic name used for metadata cache lookups 
(may include tenant
+     *                          prefix in K2; same as {@code userTopicName} in 
K1)

Review Comment:
   ```suggestion
        * @param resolvedTopicName the topic name used for metadata cache 
lookups
   ```



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.
+ */
+public class ShareGroupDLQRecordHelper {
+
+    /**
+     * In most cases we expect the records getting DLQ'ed will be single 
offsets and
+     * not complete batches. Hence, using a large upper limit while reading 
from the log
+     * would be fruitless in most cases. Therefore, the value of 1 MB has been 
chosen
+     * for the DLQ-related log reads.
+     */
+    public static final int DLQ_MAX_FETCH_BYTES = 1024 * 1024;
+
+    public static final String HEADER_DLQ_ERRORS_TOPIC = "__dlq.errors.topic";
+    public static final String HEADER_DLQ_ERRORS_PARTITION = 
"__dlq.errors.partition";
+    public static final String HEADER_DLQ_ERRORS_OFFSET = 
"__dlq.errors.offset";
+    public static final String HEADER_DLQ_ERRORS_GROUP = "__dlq.errors.group";
+    public static final String HEADER_DLQ_ERRORS_DELIVERY_COUNT = 
"__dlq.errors.delivery.count";
+    public static final String HEADER_DLQ_ERRORS_MESSAGE = 
"__dlq.errors.message";
+
+    /**
+     * Result of building DLQ records for a range of offsets, respecting 
maxMessageBytes.
+     *
+     * @param records         The built MemoryRecords containing DLQ records 
with headers
+     * @param lastOffsetIncluded The last source offset included in this batch
+     * @param recordCount     The number of individual records in the batch

Review Comment:
   nit: correct the indentation
   
   ```
        * @param records             The built MemoryRecords containing DLQ 
records with headers
        * @param lastOffsetIncluded  The last source offset included in this 
batch
        * @param recordCount         The number of individual records in the 
batch



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.
+ */
+public class ShareGroupDLQRecordHelper {
+
+    /**
+     * In most cases we expect the records getting DLQ'ed will be single 
offsets and
+     * not complete batches. Hence, using a large upper limit while reading 
from the log
+     * would be fruitless in most cases. Therefore, the value of 1 MB has been 
chosen
+     * for the DLQ-related log reads.
+     */
+    public static final int DLQ_MAX_FETCH_BYTES = 1024 * 1024;
+
+    public static final String HEADER_DLQ_ERRORS_TOPIC = "__dlq.errors.topic";
+    public static final String HEADER_DLQ_ERRORS_PARTITION = 
"__dlq.errors.partition";
+    public static final String HEADER_DLQ_ERRORS_OFFSET = 
"__dlq.errors.offset";
+    public static final String HEADER_DLQ_ERRORS_GROUP = "__dlq.errors.group";
+    public static final String HEADER_DLQ_ERRORS_DELIVERY_COUNT = 
"__dlq.errors.delivery.count";
+    public static final String HEADER_DLQ_ERRORS_MESSAGE = 
"__dlq.errors.message";
+
+    /**
+     * Result of building DLQ records for a range of offsets, respecting 
maxMessageBytes.
+     *
+     * @param records         The built MemoryRecords containing DLQ records 
with headers
+     * @param lastOffsetIncluded The last source offset included in this batch
+     * @param recordCount     The number of individual records in the batch
+     */
+    public record BuildResult(MemoryRecords records, long lastOffsetIncluded, 
int recordCount) {
+    }
+
+    /**
+     * Builds DLQ headers for a single offset.
+     *
+     * @param sourceTopic   The resolved source topic name
+     * @param partition     The source partition number
+     * @param offset        The source offset
+     * @param groupId       The share group ID
+     * @param deliveryCount Optional delivery count
+     * @param cause         Optional cause/reason for DLQ
+     * @return Array of DLQ headers
+     */
+    private static Header[] headers(
+            String sourceTopic,
+            int partition,
+            long offset,
+            String groupId,
+            Optional<Short> deliveryCount,
+            Optional<Throwable> cause
+    ) {
+        List<Header> headers = new ArrayList<>();
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_TOPIC, 
sourceTopic.getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_PARTITION, 
Integer.toString(partition).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_OFFSET, 
Long.toString(offset).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_GROUP, 
groupId.getBytes(StandardCharsets.UTF_8)));
+        deliveryCount.ifPresent(dc -> headers.add(
+                new RecordHeader(HEADER_DLQ_ERRORS_DELIVERY_COUNT, 
Short.toString(dc).getBytes(StandardCharsets.UTF_8))));
+        cause.ifPresent(c -> {
+            if (c.getMessage() != null) {
+                headers.add(new RecordHeader(HEADER_DLQ_ERRORS_MESSAGE, 
c.getMessage().getBytes(StandardCharsets.UTF_8)));
+            }
+        });
+        return headers.toArray(new Header[0]);

Review Comment:
   Would it be better to avoid unnecessary ArrayList construction and Array 
copy?
   
   ```
         String causeMessage = cause.map(Throwable::getMessage).orElse(null);
         int size = 4 + (deliveryCount.isPresent() ? 1 : 0) + (causeMessage != 
null ? 1 : 0);
   
         Header[] headers = new Header[size];
         int i = 0;
         headers[i++] = new RecordHeader(HEADER_DLQ_ERRORS_TOPIC, 
sourceTopic.getBytes(StandardCharsets.UTF_8));
         headers[i++] = new RecordHeader(HEADER_DLQ_ERRORS_PARTITION,
     Integer.toString(partition).getBytes(StandardCharsets.UTF_8));
         headers[i++] = new RecordHeader(HEADER_DLQ_ERRORS_OFFSET, 
Long.toString(offset).getBytes(StandardCharsets.UTF_8));
         headers[i++] = new RecordHeader(HEADER_DLQ_ERRORS_GROUP, 
groupId.getBytes(StandardCharsets.UTF_8));
         if (deliveryCount.isPresent()) {
             headers[i++] = new RecordHeader(HEADER_DLQ_ERRORS_DELIVERY_COUNT,
                 
Short.toString(deliveryCount.get()).getBytes(StandardCharsets.UTF_8));
         }
         if (causeMessage != null) {
             headers[i] = new RecordHeader(HEADER_DLQ_ERRORS_MESSAGE, 
causeMessage.getBytes(StandardCharsets.UTF_8));
         }
         return headers;



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.
+ */
+public class ShareGroupDLQRecordHelper {
+
+    /**
+     * In most cases we expect the records getting DLQ'ed will be single 
offsets and
+     * not complete batches. Hence, using a large upper limit while reading 
from the log
+     * would be fruitless in most cases. Therefore, the value of 1 MB has been 
chosen
+     * for the DLQ-related log reads.
+     */
+    public static final int DLQ_MAX_FETCH_BYTES = 1024 * 1024;
+
+    public static final String HEADER_DLQ_ERRORS_TOPIC = "__dlq.errors.topic";
+    public static final String HEADER_DLQ_ERRORS_PARTITION = 
"__dlq.errors.partition";
+    public static final String HEADER_DLQ_ERRORS_OFFSET = 
"__dlq.errors.offset";
+    public static final String HEADER_DLQ_ERRORS_GROUP = "__dlq.errors.group";
+    public static final String HEADER_DLQ_ERRORS_DELIVERY_COUNT = 
"__dlq.errors.delivery.count";
+    public static final String HEADER_DLQ_ERRORS_MESSAGE = 
"__dlq.errors.message";
+
+    /**
+     * Result of building DLQ records for a range of offsets, respecting 
maxMessageBytes.
+     *
+     * @param records         The built MemoryRecords containing DLQ records 
with headers
+     * @param lastOffsetIncluded The last source offset included in this batch
+     * @param recordCount     The number of individual records in the batch
+     */
+    public record BuildResult(MemoryRecords records, long lastOffsetIncluded, 
int recordCount) {
+    }
+
+    /**
+     * Builds DLQ headers for a single offset.
+     *
+     * @param sourceTopic   The resolved source topic name
+     * @param partition     The source partition number
+     * @param offset        The source offset
+     * @param groupId       The share group ID
+     * @param deliveryCount Optional delivery count
+     * @param cause         Optional cause/reason for DLQ
+     * @return Array of DLQ headers
+     */
+    private static Header[] headers(
+            String sourceTopic,
+            int partition,
+            long offset,
+            String groupId,
+            Optional<Short> deliveryCount,
+            Optional<Throwable> cause
+    ) {
+        List<Header> headers = new ArrayList<>();
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_TOPIC, 
sourceTopic.getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_PARTITION, 
Integer.toString(partition).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_OFFSET, 
Long.toString(offset).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_GROUP, 
groupId.getBytes(StandardCharsets.UTF_8)));
+        deliveryCount.ifPresent(dc -> headers.add(
+                new RecordHeader(HEADER_DLQ_ERRORS_DELIVERY_COUNT, 
Short.toString(dc).getBytes(StandardCharsets.UTF_8))));
+        cause.ifPresent(c -> {
+            if (c.getMessage() != null) {
+                headers.add(new RecordHeader(HEADER_DLQ_ERRORS_MESSAGE, 
c.getMessage().getBytes(StandardCharsets.UTF_8)));
+            }
+        });
+        return headers.toArray(new Header[0]);
+    }
+
+    /**
+     * Resolves the source topic name from a TopicIdPartition, falling back to 
the topic ID string.
+     *
+     * @param topicIdPartition The source topic-partition
+     * @param topicNameResolver Resolver that maps topic ID to name

Review Comment:
   nit: align the indentation



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQValidator.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.config.ConfigException;
+
+import java.util.Optional;
+
+/**
+ * Shared validation logic for DLQ managers ({@link ShareGroupDLQStateManager} 
and
+ * {@code K2ShareGroupDLQManager}).
+ */
+public final class ShareGroupDLQValidator {
+
+    private ShareGroupDLQValidator() {}
+
+    /**
+     * Validates the fields of a {@link ShareGroupDLQRecordParameter}.
+     *
+     * @throws IllegalArgumentException if any field is invalid
+     */
+    public static void validateParam(ShareGroupDLQRecordParameter param) {
+        String prefix = "DLQ records parameters";
+        if (param == null) {
+            throw new IllegalArgumentException(prefix + " cannot be null.");
+        }
+        if (param.groupId() == null || param.groupId().isEmpty()) {
+            throw new IllegalArgumentException(prefix + " group cannot be null 
or empty.");
+        }
+        if (param.topicIdPartition() == null) {
+            throw new IllegalArgumentException(prefix + " topic/partition data 
cannot be null or empty.");
+        }
+        if (param.topicIdPartition().topicId() == null) {
+            throw new IllegalArgumentException(prefix + " topic id data cannot 
be null or empty.");
+        }
+        if (param.topicIdPartition().partition() < 0) {
+            throw new IllegalArgumentException(prefix + " partition cannot be 
negative.");
+        }
+        if (param.firstOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " first offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < param.firstOffset()) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be less than first offset.");
+        }
+    }
+
+    /**
+     * Validates DLQ topic configuration. Checks that the topic name does not 
start with {@code __},
+     * that DLQ is enabled on the topic (if it exists), and that the topic 
name complies with the
+     * configured prefix.
+     *
+     * <p>Callers are responsible for checking that the topic name is present 
in the config (non-empty)
+     * before calling this method, and for any implementation-specific checks 
(e.g., K1 auto-create).
+     *
+     * @param groupId           the share group ID, for error messages
+     * @param userTopicName     the raw DLQ topic name from config (without 
tenant prefix)

Review Comment:
   ```suggestion
        * @param userTopicName     the raw DLQ topic name from config
   ```



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQValidator.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.config.ConfigException;
+
+import java.util.Optional;
+
+/**
+ * Shared validation logic for DLQ managers ({@link ShareGroupDLQStateManager} 
and
+ * {@code K2ShareGroupDLQManager}).

Review Comment:
   ```suggestion
    * Shared validation logic for DLQ manager.
   ```



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.

Review Comment:
   ```suggestion
   ```



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQRecordHelper.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.TopicIdPartition;
+import org.apache.kafka.common.Uuid;
+import org.apache.kafka.common.compress.Compression;
+import org.apache.kafka.common.header.Header;
+import org.apache.kafka.common.header.internals.RecordHeader;
+import org.apache.kafka.common.record.internal.DefaultRecord;
+import org.apache.kafka.common.record.internal.DefaultRecordBatch;
+import org.apache.kafka.common.record.internal.MemoryRecords;
+import org.apache.kafka.common.record.internal.Record;
+import org.apache.kafka.common.record.internal.SimpleRecord;
+import org.apache.kafka.common.utils.Time;
+import org.apache.kafka.server.share.LogReader;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+/**
+ * Shared helper for DLQ record building and source-record fetching.
+ * Used by both the K1 ({@link ShareGroupDLQStateManager}) and K2 DLQ manager 
implementations.
+ */
+public class ShareGroupDLQRecordHelper {
+
+    /**
+     * In most cases we expect the records getting DLQ'ed will be single 
offsets and
+     * not complete batches. Hence, using a large upper limit while reading 
from the log
+     * would be fruitless in most cases. Therefore, the value of 1 MB has been 
chosen
+     * for the DLQ-related log reads.
+     */
+    public static final int DLQ_MAX_FETCH_BYTES = 1024 * 1024;
+
+    public static final String HEADER_DLQ_ERRORS_TOPIC = "__dlq.errors.topic";
+    public static final String HEADER_DLQ_ERRORS_PARTITION = 
"__dlq.errors.partition";
+    public static final String HEADER_DLQ_ERRORS_OFFSET = 
"__dlq.errors.offset";
+    public static final String HEADER_DLQ_ERRORS_GROUP = "__dlq.errors.group";
+    public static final String HEADER_DLQ_ERRORS_DELIVERY_COUNT = 
"__dlq.errors.delivery.count";
+    public static final String HEADER_DLQ_ERRORS_MESSAGE = 
"__dlq.errors.message";
+
+    /**
+     * Result of building DLQ records for a range of offsets, respecting 
maxMessageBytes.
+     *
+     * @param records         The built MemoryRecords containing DLQ records 
with headers
+     * @param lastOffsetIncluded The last source offset included in this batch
+     * @param recordCount     The number of individual records in the batch
+     */
+    public record BuildResult(MemoryRecords records, long lastOffsetIncluded, 
int recordCount) {
+    }
+
+    /**
+     * Builds DLQ headers for a single offset.
+     *
+     * @param sourceTopic   The resolved source topic name
+     * @param partition     The source partition number
+     * @param offset        The source offset
+     * @param groupId       The share group ID
+     * @param deliveryCount Optional delivery count
+     * @param cause         Optional cause/reason for DLQ
+     * @return Array of DLQ headers
+     */
+    private static Header[] headers(
+            String sourceTopic,
+            int partition,
+            long offset,
+            String groupId,
+            Optional<Short> deliveryCount,
+            Optional<Throwable> cause
+    ) {
+        List<Header> headers = new ArrayList<>();
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_TOPIC, 
sourceTopic.getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_PARTITION, 
Integer.toString(partition).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_OFFSET, 
Long.toString(offset).getBytes(StandardCharsets.UTF_8)));
+        headers.add(new RecordHeader(HEADER_DLQ_ERRORS_GROUP, 
groupId.getBytes(StandardCharsets.UTF_8)));
+        deliveryCount.ifPresent(dc -> headers.add(
+                new RecordHeader(HEADER_DLQ_ERRORS_DELIVERY_COUNT, 
Short.toString(dc).getBytes(StandardCharsets.UTF_8))));
+        cause.ifPresent(c -> {
+            if (c.getMessage() != null) {
+                headers.add(new RecordHeader(HEADER_DLQ_ERRORS_MESSAGE, 
c.getMessage().getBytes(StandardCharsets.UTF_8)));
+            }
+        });
+        return headers.toArray(new Header[0]);
+    }
+
+    /**
+     * Resolves the source topic name from a TopicIdPartition, falling back to 
the topic ID string.
+     *
+     * @param topicIdPartition The source topic-partition
+     * @param topicNameResolver Resolver that maps topic ID to name
+     * @return The resolved topic name
+     */
+    public static String resolveSourceTopicName(TopicIdPartition 
topicIdPartition, java.util.function.Function<Uuid, Optional<String>> 
topicNameResolver) {
+        String recordTopicName = topicIdPartition.topic();
+        if (recordTopicName == null || recordTopicName.isEmpty()) {
+            // If topic name lookup fails, use topic id as a String in the 
header.
+            recordTopicName = 
topicNameResolver.apply(topicIdPartition.topicId()).orElse(topicIdPartition.topicId().toString());
+        }
+        return recordTopicName;
+    }
+
+    /**
+     * Computes the destination DLQ partition from the source partition.
+     *
+     * @param sourcePartition   The source partition number
+     * @param numDlqPartitions  The number of partitions in the DLQ topic
+     * @return The destination DLQ partition number
+     */
+    public static int dlqDestinationPartition(int sourcePartition, int 
numDlqPartitions) {
+        return sourcePartition % numDlqPartitions;
+    }
+
+    /**
+     * Builds DLQ MemoryRecords for a range of offsets, respecting 
maxMessageBytes for batch splitting.
+     *
+     * @param param              The DLQ record parameter with offset range 
and metadata
+     * @param resolvedRecordData Map of source offset to source Record (can be 
incomplete)
+     * @param nextOffsetToSend   The first offset to include in this batch
+     * @param lastResolvedOffset The last offset that was resolved by the 
fetcher
+     * @param maxMessageBytes    Maximum batch size in bytes
+     * @param time               Time instance for wall-clock timestamps
+     * @param sourceTopic        The resolved source topic name
+     * @return BuildResult containing the MemoryRecords and the last offset 
included
+     */
+    public static BuildResult buildDLQRecords(
+            ShareGroupDLQRecordParameter param,
+            Map<Long, Record> resolvedRecordData,
+            long nextOffsetToSend,
+            long lastResolvedOffset,
+            int maxMessageBytes,
+            Time time,
+            String sourceTopic
+    ) {
+        // In most cases the offset range is a single offset (see 
DLQ_MAX_FETCH_BYTES). Track the
+        // running batch size incrementally via DefaultRecord.sizeInBytes() - 
the same formula
+        // MemoryRecords itself uses - instead of re-serializing the whole 
batch-so-far on every
+        // offset, which would make this loop quadratic in the number of 
offsets.
+        List<SimpleRecord> simpleRecords = new ArrayList<>();
+        int batchSize = DefaultRecordBatch.RECORD_BATCH_OVERHEAD;
+        Long baseTimestamp = null;
+        // Capped at lastResolvedOffsetThisRound, not just param.lastOffset(): 
offsets beyond it were
+        // never attempted by this round's fetch and must stay untouched for a 
fresh round to retry,
+        // rather than being packed in here as headers-only just because they 
have no map entry yet.
+        // Floored at nextOffsetToSend itself (mirroring the single-record 
floor below for the
+        // size-exceeds-limit case): a fetch that resolved nothing at all for 
this round - e.g. a read
+        // that failed outright - would otherwise leave this loop with zero 
iterations, producing an
+        // empty record batch, which the broker rejects outright. Sending 
nextOffsetToSend alone,
+        // headers-only, guarantees forward progress even when nothing could 
be resolved.
+        long roundEnd = Math.max(nextOffsetToSend, 
Math.min(param.lastOffset(), lastResolvedOffset));
+
+        for (long offset = nextOffsetToSend; offset <= roundEnd; offset++) {
+            // Must be wall-clock (epoch) time: log retention decides whether 
to delete this
+            // record's segment by comparing its timestamp against the current 
wall-clock time.
+            long timestamp = time.milliseconds();
+            ByteBuffer key = null;
+            ByteBuffer value = null;
+            Record record = resolvedRecordData.get(offset);
+            if (record != null) {
+                key = record.hasKey() ? record.key() : null;
+                value = record.hasValue() ? record.value() : null;
+            }
+            Header[] recordHeaders = headers(sourceTopic, 
param.topicIdPartition().partition(),
+                    offset, param.groupId(), param.deliveryCount(), 
param.cause());
+            if (baseTimestamp == null) {
+                baseTimestamp = timestamp;
+            }
+            int recordSize = DefaultRecord.sizeInBytes(simpleRecords.size(), 
timestamp - baseTimestamp, key, value, recordHeaders);
+
+            if (batchSize + recordSize > maxMessageBytes && 
!simpleRecords.isEmpty()) {
+                // Adding this record would exceed the limit and the batch 
already has at least one
+                // record - stop here and send the rest in a follow-up request.
+                break;
+            }
+            simpleRecords.add(new SimpleRecord(timestamp, key, value, 
recordHeaders));
+            batchSize += recordSize;
+            if (batchSize > maxMessageBytes) {
+                // A single record (with its DLQ headers) already exceeds the 
limit on its own;
+                // nothing to be gained by holding it back, so send it and let 
the broker
+                // enforce/report the ultimate limit for this one, rather than 
stalling forever.
+                break;
+            }
+        }
+
+        long lastOffsetIncluded = nextOffsetToSend + simpleRecords.size() - 1;
+        MemoryRecords records = MemoryRecords.withRecords(
+                Compression.NONE,
+                simpleRecords.toArray(new SimpleRecord[]{})
+        );
+        return new BuildResult(records, lastOffsetIncluded, 
simpleRecords.size());
+    }
+
+    /**
+     * Optionally fetches source records for DLQ copy-record mode. If 
copy-record is disabled
+     * for the group, returns an empty result immediately.
+     *
+     * @param param              The DLQ record parameter
+     * @param fromOffset         The first offset to fetch from
+     * @param cacheHelper        Metadata cache helper for config lookups
+     * @param logReader          Log reader for fetching source records
+     * @param time               Time instance
+     * @param topicNameResolver  Resolves the raw DLQ topic name to the name 
used in the metadata
+     *                           cache. K2 prepends the tenant prefix; K1 
passes {@link Function#identity()}.

Review Comment:
   ```suggestion
        * @param topicNameResolver  Resolves the raw DLQ topic name to the name 
used in the metadata cache.
   ```



##########
server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQValidator.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.kafka.server.share.dlq;
+
+import org.apache.kafka.common.config.ConfigException;
+
+import java.util.Optional;
+
+/**
+ * Shared validation logic for DLQ managers ({@link ShareGroupDLQStateManager} 
and
+ * {@code K2ShareGroupDLQManager}).
+ */
+public final class ShareGroupDLQValidator {
+
+    private ShareGroupDLQValidator() {}
+
+    /**
+     * Validates the fields of a {@link ShareGroupDLQRecordParameter}.
+     *
+     * @throws IllegalArgumentException if any field is invalid
+     */
+    public static void validateParam(ShareGroupDLQRecordParameter param) {
+        String prefix = "DLQ records parameters";
+        if (param == null) {
+            throw new IllegalArgumentException(prefix + " cannot be null.");
+        }
+        if (param.groupId() == null || param.groupId().isEmpty()) {
+            throw new IllegalArgumentException(prefix + " group cannot be null 
or empty.");
+        }
+        if (param.topicIdPartition() == null) {
+            throw new IllegalArgumentException(prefix + " topic/partition data 
cannot be null or empty.");
+        }
+        if (param.topicIdPartition().topicId() == null) {
+            throw new IllegalArgumentException(prefix + " topic id data cannot 
be null or empty.");
+        }
+        if (param.topicIdPartition().partition() < 0) {
+            throw new IllegalArgumentException(prefix + " partition cannot be 
negative.");
+        }
+        if (param.firstOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " first offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < 0) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be negative.");
+        }
+        if (param.lastOffset() < param.firstOffset()) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be less than first offset.");
+        }
+    }
+
+    /**
+     * Validates DLQ topic configuration. Checks that the topic name does not 
start with {@code __},
+     * that DLQ is enabled on the topic (if it exists), and that the topic 
name complies with the
+     * configured prefix.
+     *
+     * <p>Callers are responsible for checking that the topic name is present 
in the config (non-empty)
+     * before calling this method, and for any implementation-specific checks 
(e.g., K1 auto-create).

Review Comment:
   ```suggestion
        * before calling this method, and for any implementation-specific 
checks (e.g., auto-create).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to