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

apoorvmittal10 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 bd5fcec64e0 MINOR: Fix DLQ records using a non-epoch clock for their 
timestamp (#22849)
bd5fcec64e0 is described below

commit bd5fcec64e0a2ec057ca9f88da345926d8c58342
Author: Sanskar Jhajharia <[email protected]>
AuthorDate: Thu Jul 16 19:27:21 2026 +0530

    MINOR: Fix DLQ records using a non-epoch clock for their timestamp (#22849)
    
    ### Summary
    
    Share-group DLQ records (KIP-1191) were built with `Time#hiResClockMs()`
    as their timestamp instead of `Time#milliseconds()`. `hiResClockMs()` is
    `System.nanoTime()` converted to milliseconds, explicitly documented as
    measuring elapsed durations from an arbitrary, non-epoch origin, not
    wall-clock time. Every DLQ record ended up stamped with something like
    `332800244 (~4 days after the Unix epoch)` instead of a real 2026
    timestamp.
    
    Kafka's log retention decides whether to delete a segment by comparing
    each record's timestamp against the current wall-clock time. With a
    near-epoch timestamp, that comparison is always "yes, delete this"
    regardless of the configured `retention.ms`. So the DLQ record is
    produced successfully, and then deleted by the very next retention
    check, typically well under a second later.
    
    ### Impact
    
    This affects every DLQ write, on any broker. Whether it's visible in
    practice depends entirely on how fast something reads the DLQ topic
    after the record lands relative to `log.retention.check.interval.ms`
    (default 5 minutes, but frequently lowered in tiered-storage/testing
    configs) — so the practical exposure ranges from "DLQ appears completely
    empty" to "DLQ briefly has data that vanishes moments later," neither of
    which produces any error, warning, or log line pointing at the cause.
    
    ### Why this was hard to find
    
    - The write path itself is silent on success: the produce request
    completes normally (broker ack, high watermark advances), so there's no
    exception or log line anywhere suggesting anything is wrong.
    - The existing JUnit coverage (`ShareConsumerDLQTest`) reads the DLQ
    topic via a bare `consumer.assign() + seekToBeginning() + poll()` —
    near-zero setup latency. It happened to read the records within a few
    hundred milliseconds, comfortably beating the next retention cycle every
    time. It was passing by winning a timing race, not because the write
    path was correct.
    - A newer ducktape system test, whose DLQ reader goes through a real
    consumer-group `subscribe()` (requiring a multi-second group
    join/rebalance before it can fetch anything), reliably lost that same
    race — which is what surfaced this as worth investigating rather than
    dismissing as test-infra flakiness.
    - The existing unit tests (`ShareGroupDLQStateManagerTest`) never
    asserted on the actual timestamp value of produced records, only on
    headers/key/value — so there was no test that could have caught a wrong
    clock being used.
    
    ### Fix
    
    One-line change: `time.hiResClockMs() -> time.milliseconds()`
    
    Added a regression test (`assertDlqRecordTimestampsAreWallClock`, wired
    into `testDlqHappyPathExistingTopic`) asserting the produced record's
    timestamp matches Time#milliseconds(). Confirmed it fails against the
    pre-fix code and passes with the fix, closing the coverage gap above.
    
    Reviewers: Sushant Mahajan <[email protected]>, Apoorv Mittal
     <[email protected]>, Andrew Schofield <[email protected]>
---
 .../server/share/dlq/ShareGroupDLQStateManager.java   |  4 +++-
 .../share/dlq/ShareGroupDLQStateManagerTest.java      | 19 +++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
 
b/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
index 8cad732fbad..52753feb9d0 100644
--- 
a/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
+++ 
b/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
@@ -412,7 +412,9 @@ public class ShareGroupDLQStateManager {
             int batchSize = DefaultRecordBatch.RECORD_BATCH_OVERHEAD;
             Long baseTimestamp = null;
             for (long offset = nextOffsetToSend; offset <= param.lastOffset(); 
offset++) {
-                long timestamp = time.hiResClockMs();
+                // 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 = originalRecordData.get(offset);
diff --git 
a/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
index 1c37610f732..307003022b4 100644
--- 
a/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
+++ 
b/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
@@ -543,11 +543,30 @@ class ShareGroupDLQStateManagerTest {
                 HEADER_DLQ_ERRORS_MESSAGE, "simulated cause"
             ), List.of(), List.of())
         ));
+        assertDlqRecordTimestampsAreWallClock(capturedProduces.get(0));
         verify(mockMetrics).recordDLQProduce(GROUP_ID);
         verify(mockMetrics).recordDLQRecordWrite(GROUP_ID, 3);
         verify(mockMetrics, never()).recordDLQProduceFailed(any());
     }
 
+    /**
+     * Asserts every record in the captured produce request carries wall-clock 
(epoch) time as its
+     * timestamp, matching {@link #MOCK_TIME}'s {@link 
org.apache.kafka.common.utils.Time#milliseconds()}.
+     * DLQ record timestamps must reflect real time, since log retention 
decides whether to delete a
+     * segment by comparing each record's timestamp against the current 
wall-clock time.
+     */
+    private static void assertDlqRecordTimestampsAreWallClock(ProduceRequest 
request) {
+        long expectedTimestampMs = MOCK_TIME.milliseconds();
+        for (ProduceRequestData.TopicProduceData topic : 
request.data().topicData()) {
+            for (ProduceRequestData.PartitionProduceData partition : 
topic.partitionData()) {
+                for (Record record : ((MemoryRecords) 
partition.records()).records()) {
+                    assertEquals(expectedTimestampMs, record.timestamp(),
+                        "DLQ record timestamp should be wall-clock time, not 
an arbitrary-origin clock");
+                }
+            }
+        }
+    }
+
     @Test
     public void testDlqTopicPrefixEmptyStringSkipsPrefixCheck() throws 
Exception {
         ShareGroupDLQMetadataCacheHelper cacheHelper = 
mock(ShareGroupDLQMetadataCacheHelper.class);

Reply via email to