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

smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 0c349a3f541 HDDS-15356. Make multi-buffer chunk checksum 
allocation-free (#10350)
0c349a3f541 is described below

commit 0c349a3f54166a15a061b92041831194bd89a01d
Author: Siyao Meng <[email protected]>
AuthorDate: Wed Aug 12 18:39:23 2026 -0700

    HDDS-15356. Make multi-buffer chunk checksum allocation-free (#10350)
---
 .../org/apache/hadoop/ozone/common/Checksum.java   | 186 ++++++++++++++-------
 .../apache/hadoop/ozone/common/ChecksumCache.java  | 117 +++++++++----
 .../hadoop/ozone/common/utils/BufferUtils.java     |  23 +++
 .../apache/hadoop/ozone/common/TestChecksum.java   |  17 ++
 .../hadoop/ozone/common/TestChecksumCache.java     |  73 +++++++-
 .../ozone/common/TestChecksumMultiBuffer.java      |  97 +++++++++++
 .../hadoop/ozone/common/utils/TestBufferUtils.java |  81 +++++++++
 .../ozone/container/keyvalue/KeyValueHandler.java  |   4 +-
 8 files changed, 490 insertions(+), 108 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
index 6a530cacc9a..f4010497774 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
@@ -24,7 +24,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Function;
 import java.util.function.Supplier;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
 import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
@@ -53,57 +52,108 @@ public class Checksum {
    */
   private final ChecksumCache checksumCache;
 
-  private static Function<ByteBuffer, ByteString> newMessageDigestFunction(
-      String algorithm) {
-    final MessageDigest md;
+  private static MessageDigest newMessageDigest(String algorithm) {
     try {
-      md = MessageDigest.getInstance(algorithm);
+      return MessageDigest.getInstance(algorithm);
     } catch (NoSuchAlgorithmException e) {
       throw new IllegalStateException(
           "Failed to get MessageDigest for " + algorithm,  e);
     }
-    return data -> {
-      md.reset();
-      md.update(data);
-      return ByteString.copyFrom(md.digest());
-    };
   }
 
   public static ByteString int2ByteString(int n) {
     return UnsafeByteOperations.unsafeWrap(IntegerCodec.get().toByteArray(n));
   }
 
-  private static Function<ByteBuffer, ByteString> 
newChecksumByteBufferFunction(
-      Supplier<ChecksumByteBuffer> constructor) {
-    final ChecksumByteBuffer algorithm = constructor.get();
-    return data -> {
-      algorithm.reset();
-      algorithm.update(data);
-      return int2ByteString((int)algorithm.getValue());
+  /**
+   * Streaming checksum strategy: feed multiple ByteBuffer slices via
+   * {@link #update}, then read the result via {@link #finish}, then
+   * {@link #reset} to start a new window. Used by both the no-cache and
+   * cache compute paths to avoid the {@code byte[bytesPerChecksum]}
+   * allocation that {@code ChunkBuffer.iterate} performs whenever a
+   * checksum window straddles multiple underlying buffers - the
+   * {@link ChecksumByteBuffer#update(ByteBuffer)} and
+   * {@link MessageDigest#update(ByteBuffer)} contracts both define
+   * incremental updates as byte-equivalent to a single update over the
+   * concatenation.
+   */
+  interface StreamingChecksum {
+    void reset();
+
+    void update(ByteBuffer slice);
+
+    ByteString finish();
+  }
+
+  private static StreamingChecksum streamingCrc(
+      Supplier<ChecksumByteBuffer> ctor) {
+    final ChecksumByteBuffer cb = ctor.get();
+    return new StreamingChecksum() {
+      @Override
+      public void reset() {
+        cb.reset();
+      }
+
+      @Override
+      public void update(ByteBuffer slice) {
+        cb.update(slice);
+      }
+
+      @Override
+      public ByteString finish() {
+        return int2ByteString((int) cb.getValue());
+      }
+    };
+  }
+
+  private static StreamingChecksum streamingDigest(String algorithm) {
+    final MessageDigest md = newMessageDigest(algorithm);
+    return new StreamingChecksum() {
+      @Override
+      public void reset() {
+        md.reset();
+      }
+
+      @Override
+      public void update(ByteBuffer slice) {
+        md.update(slice);
+      }
+
+      @Override
+      public ByteString finish() {
+        // The JCA SPI does not guarantee exclusive ownership of the digest
+        // array, while Protobuf unsafeWrap requires exclusive, immutable
+        // ownership. Copying 16 bytes for MD5 or 32 bytes for SHA-256 is 
cheap.
+        return ByteString.copyFrom(md.digest());
+      }
     };
   }
 
   /** The algorithms for {@link ChecksumType}. */
   enum Algorithm {
-    NONE(() -> data -> ByteString.EMPTY),
-    CRC32(() ->
-        newChecksumByteBufferFunction(ChecksumByteBufferFactory::crc32Impl)),
-    CRC32C(() ->
-        newChecksumByteBufferFunction(ChecksumByteBufferFactory::crc32CImpl)),
-    SHA256(() -> newMessageDigestFunction("SHA-256")),
-    MD5(() -> newMessageDigestFunction("MD5"));
-
-    private final Supplier<Function<ByteBuffer, ByteString>> constructor;
+    // NONE is reachable via Algorithm.valueOf(ChecksumType.NONE) only if
+    // computeChecksum's NONE short-circuit is bypassed; throw to surface
+    // such a misuse rather than silently producing empty checksums.
+    NONE(() -> {
+      throw new UnsupportedOperationException(
+          "ChecksumType.NONE has no StreamingChecksum");
+    }),
+    CRC32(() -> streamingCrc(ChecksumByteBufferFactory::crc32Impl)),
+    CRC32C(() -> streamingCrc(ChecksumByteBufferFactory::crc32CImpl)),
+    SHA256(() -> streamingDigest("SHA-256")),
+    MD5(() -> streamingDigest("MD5"));
+
+    private final Supplier<StreamingChecksum> constructor;
 
     static Algorithm valueOf(ChecksumType type) {
       return valueOf(type.name());
     }
 
-    Algorithm(Supplier<Function<ByteBuffer, ByteString>> constructor) {
+    Algorithm(Supplier<StreamingChecksum> constructor) {
       this.constructor = constructor;
     }
 
-    Function<ByteBuffer, ByteString> newChecksumFunction() {
+    StreamingChecksum newStreamingChecksum() {
       return constructor.get();
     }
   }
@@ -218,65 +268,71 @@ public ChecksumData computeChecksum(ChunkBuffer data)
     return computeChecksum(data, false);
   }
 
+  /**
+   * This method does not advance the positions of {@code data}'s underlying
+   * buffers. Both the no-cache and cache paths slice via
+   * {@link ByteBuffer#duplicate()}.
+   */
   public ChecksumData computeChecksum(ChunkBuffer data, boolean useCache)
       throws OzoneChecksumException {
     if (checksumType == ChecksumType.NONE) {
-      // Since type is set to NONE, we do not need to compute the checksums
       return new ChecksumData(checksumType, bytesPerChecksum);
     }
 
-    final Function<ByteBuffer, ByteString> function;
+    final StreamingChecksum algo;
     try {
-      function = Algorithm.valueOf(checksumType).newChecksumFunction();
+      algo = Algorithm.valueOf(checksumType).newStreamingChecksum();
     } catch (Exception e) {
-      throw new OzoneChecksumException("Failed to get the checksum function 
for " + checksumType, e);
+      throw new OzoneChecksumException(
+          "Failed to create streaming checksum for " + checksumType, e);
     }
 
-    final List<ByteString> checksumList;
-    if (checksumCache == null || !useCache) {
-      // When checksumCache is not enabled:
-      // Checksum is computed for each bytesPerChecksum number of bytes of data
-      // starting at offset 0. The last checksum might be computed for the
-      // remaining data with length less than bytesPerChecksum.
-      checksumList = new ArrayList<>();
-      for (ByteBuffer b : data.iterate(bytesPerChecksum)) {
-        checksumList.add(computeChecksum(b, function, bytesPerChecksum));  // 
merge this?
-      }
-    } else {
-      // When checksumCache is enabled:
-      // We only need to update the last checksum in the cache, then pass it 
along.
-      checksumList = checksumCache.computeChecksum(data, function);
-    }
+    final List<ByteString> checksumList = (checksumCache == null || !useCache)
+        ? computeChecksumDirect(data, algo)
+        : checksumCache.computeChecksum(data, algo, bytesPerChecksum);
     return new ChecksumData(checksumType, bytesPerChecksum, checksumList);
   }
 
   /**
-   * Compute checksum using the algorithm for the data upto the max length.
-   * @param data input data
-   * @param function the checksum function
-   * @param maxLength the max length of data
-   * @return computed checksum ByteString
+   * Walk {@code data}'s underlying ByteBuffer list, slicing each window of
+   * {@link #bytesPerChecksum} bytes via {@link ByteBuffer#duplicate()} and
+   * feeding slices to {@code algo}.  No linearization byte[] is allocated
+   * when a window straddles multiple buffers.
    */
-  protected static ByteString computeChecksum(ByteBuffer data,
-      Function<ByteBuffer, ByteString> function, int maxLength) {
-    final int limit = data.limit();
-    try {
-      final int maxIndex = data.position() + maxLength;
-      if (limit > maxIndex) {
-        data.limit(maxIndex);
+  private List<ByteString> computeChecksumDirect(ChunkBuffer data,
+      StreamingChecksum algo) {
+    final int dataLength = data.remaining();
+    final int checksumCount = dataLength == 0 ? 0 : 1 + (dataLength - 1) / 
bytesPerChecksum;
+    final List<ByteString> result = new ArrayList<>(checksumCount);
+    int windowRemaining = bytesPerChecksum;
+    algo.reset();
+
+    for (ByteBuffer src : data.asByteBufferList()) {
+      int srcPos = src.position();
+      final int srcLim = src.limit();
+      while (srcPos < srcLim) {
+        final int n = Math.min(srcLim - srcPos, windowRemaining);
+        algo.update(BufferUtils.slice(src, srcPos, n));
+        srcPos += n;
+        windowRemaining -= n;
+        if (windowRemaining == 0) {
+          result.add(algo.finish());
+          algo.reset();
+          windowRemaining = bytesPerChecksum;
+        }
       }
-      return function.apply(data);
-    } finally {
-      data.limit(limit);
     }
+    if (windowRemaining < bytesPerChecksum) {
+      // Unaligned trailing window.
+      result.add(algo.finish());
+    }
+    return result;
   }
 
   public static void verifySingleChecksum(ByteBuffer buffer, int offset, int 
bytesPerChecksum,
       ByteString checksum, ChecksumType checksumType) throws 
OzoneChecksumException {
-    final ByteBuffer duplicated = buffer.duplicate();
-    duplicated.position(offset).limit(offset + bytesPerChecksum);
     final ChecksumData cd = new ChecksumData(checksumType, bytesPerChecksum, 
Collections.singletonList(checksum));
-    verifyChecksum(duplicated, cd, 0);
+    verifyChecksum(BufferUtils.slice(buffer, offset, bytesPerChecksum), cd, 0);
   }
 
   /**
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChecksumCache.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChecksumCache.java
index 3b96b35eba6..b59fbdb3f04 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChecksumCache.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChecksumCache.java
@@ -17,10 +17,12 @@
 
 package org.apache.hadoop.ozone.common;
 
+import com.google.common.base.Preconditions;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.function.Function;
+import org.apache.hadoop.ozone.common.Checksum.StreamingChecksum;
+import org.apache.hadoop.ozone.common.utils.BufferUtils;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -65,58 +67,105 @@ public List<ByteString> getChecksums() {
     return checksums;
   }
 
-  public List<ByteString> computeChecksum(ChunkBuffer data, 
Function<ByteBuffer, ByteString> function) {
-    // Indicates how much data the current chunk buffer holds
-    final int currChunkLength = data.limit();
+  /**
+   * Recompute checksums for the windows that have changed since the last
+   * call: bytes {@code [ciStart * bytesPerChecksum, currChunkLength)} where
+   * {@code ciStart = prevChunkLength / bytesPerChecksum} (the index of the
+   * first window whose result may have changed - either the previously-
+   * partial last window now has more bytes, or new full windows have been
+   * appended).
+   *
+   * <p>Walks {@code data}'s underlying buffer list directly (no
+   * {@code iterate()} byte[] linearization) and feeds slices to {@code algo}
+   * incrementally; the cached prefix is skipped via index arithmetic so
+   * those bytes are never re-fed.
+   */
+  List<ByteString> computeChecksum(ChunkBuffer data,
+      StreamingChecksum algo, int chksumSize) {
+    if (chksumSize != bytesPerChecksum) {
+      throw new IllegalArgumentException("bytesPerChecksum mismatch: cache="
+          + bytesPerChecksum + " call=" + chksumSize);
+    }
+    final int currChunkLength = data.remaining();
 
     if (currChunkLength == prevChunkLength) {
-      LOG.debug("ChunkBuffer data limit same as last time ({}). No new 
checksums need to be computed", prevChunkLength);
+      LOG.debug("ChunkBuffer data length same as last time ({}). "
+          + "No new checksums need to be computed", prevChunkLength);
       return checksums;
     }
-
-    // Sanity check
     if (currChunkLength < prevChunkLength) {
-      // If currChunkLength <= lastChunkLength, it indicates a bug that needs 
to be addressed.
-      // It means BOS has not properly clear()ed the cache when a new chunk is 
started in that code path.
-      throw new IllegalArgumentException("ChunkBuffer data limit (" + 
currChunkLength + ")" +
-          " must not be smaller than last time (" + prevChunkLength + ")");
+      // Indicates a bug: BOS did not clear() the cache before starting a new 
chunk.
+      throw new IllegalArgumentException("ChunkBuffer data length (" + 
currChunkLength + ")"
+          + " must not be smaller than last time (" + prevChunkLength + ")");
     }
 
-    // One or more checksums need to be computed
-
-    // Start of the checksum index that need to be (re)computed
+    // Index of the first window that needs (re)computing.
     final int ciStart = prevChunkLength / bytesPerChecksum;
-    final int ciEnd = currChunkLength / bytesPerChecksum + (currChunkLength % 
bytesPerChecksum == 0 ? 0 : 1);
-    int i = 0;
-    for (ByteBuffer b : data.iterate(bytesPerChecksum)) {
-      if (i < ciStart) {
-        i++;
+    final int ciEnd = currChunkLength / bytesPerChecksum
+        + (currChunkLength % bytesPerChecksum == 0 ? 0 : 1);
+    // Bytes to skip (the cached full windows preceding ciStart).
+    long bytesToSkip = (long) ciStart * bytesPerChecksum;
+
+    int i = ciStart;
+    int windowRemaining = bytesPerChecksum;
+    algo.reset();
+    long position = 0;
+
+    for (ByteBuffer src : data.asByteBufferList()) {
+      int srcPos = src.position();
+      final int srcLim = src.limit();
+      final int srcLen = srcLim - srcPos;
+
+      // Fast-forward through buffers that lie entirely within the cached
+      // prefix.
+      if (position + srcLen <= bytesToSkip) {
+        position += srcLen;
         continue;
       }
-
-      // variable i can either point to:
-      // 1. the last element in the list -- in which case the checksum needs 
to be updated
-      // 2. one after the last element   -- in which case a new checksum needs 
to be added
-      assert i == checksums.size() - 1 || i == checksums.size();
-
-      // TODO: Furthermore for CRC32/CRC32C, it can be even more efficient by 
updating the last checksum byte-by-byte.
-      final ByteString checksum = Checksum.computeChecksum(b, function, 
bytesPerChecksum);
-      if (i == checksums.size()) {
-        checksums.add(checksum);
-      } else {
-        checksums.set(i, checksum);
+      // First buffer that crosses into the not-yet-cached region: advance
+      // srcPos to the boundary.
+      if (position < bytesToSkip) {
+        srcPos += (int) (bytesToSkip - position);
+        position = bytesToSkip;
       }
 
-      i++;
+      while (srcPos < srcLim) {
+        final int n = Math.min(srcLim - srcPos, windowRemaining);
+        algo.update(BufferUtils.slice(src, srcPos, n));
+        srcPos += n;
+        position += n;
+        windowRemaining -= n;
+        if (windowRemaining == 0) {
+          storeChecksum(i++, algo.finish());
+          algo.reset();
+          windowRemaining = bytesPerChecksum;
+        }
+      }
+    }
+    if (windowRemaining < bytesPerChecksum) {
+      // Unaligned trailing window.
+      storeChecksum(i++, algo.finish());
     }
 
-    // Sanity check
     if (i != ciEnd) {
       throw new IllegalStateException("ChecksumCache: Checksum index end does 
not match expectation");
     }
 
-    // Update last written index
     prevChunkLength = currChunkLength;
     return checksums;
   }
+
+  private void storeChecksum(int i, ByteString cs) {
+    // i can either point to the last cached element (recompute - the
+    // previously-partial trailing window) or one past it (append a new
+    // checksum for newly-arrived bytes).
+    Preconditions.checkState(
+        i == checksums.size() - 1 || i == checksums.size(),
+        "Unexpected checksum index %s for cache size %s", i, checksums.size());
+    if (i == checksums.size()) {
+      checksums.add(cs);
+    } else {
+      checksums.set(i, cs);
+    }
+  }
 }
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/utils/BufferUtils.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/utils/BufferUtils.java
index 794504b1ef3..b05d2dd9a45 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/utils/BufferUtils.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/utils/BufferUtils.java
@@ -23,6 +23,7 @@
 import java.nio.channels.GatheringByteChannel;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,6 +69,28 @@ public static ByteBuffer[] assignByteBuffers(long totalLen,
     return dataBuffers;
   }
 
+  /**
+   * Return a non-copying {@link ByteBuffer#duplicate()} of {@code src} that
+   * covers exactly {@code [position, position + length)}. Read-only-ness
+   * and direct/heap kind are inherited from {@code src}.
+   */
+  public static ByteBuffer slice(ByteBuffer src, int position, int length) {
+    Objects.requireNonNull(src, "src must not be null");
+    Preconditions.checkArgument(position >= 0,
+        "position (%s) must not be negative", position);
+    Preconditions.checkArgument(length >= 0,
+        "length (%s) must not be negative", length);
+    Preconditions.checkArgument(position <= src.limit(),
+        "position (%s) exceeds source limit (%s)", position, src.limit());
+    Preconditions.checkArgument(length <= src.limit() - position,
+        "position (%s) + length (%s) exceeds source limit (%s)",
+        position, length, src.limit());
+    final ByteBuffer slice = src.duplicate();
+    slice.position(position);
+    slice.limit(position + length);
+    return slice;
+  }
+
   /**
    * Return a read only ByteBuffer list for the input ByteStrings list.
    */
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
index eb11fe53d9c..ce597971c04 100644
--- 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
@@ -20,10 +20,14 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -100,4 +104,17 @@ public void 
testChecksumMismatchForDifferentChecksumTypes(boolean useChecksumCac
     // The two checksums should not match as they have different types
     assertNotEquals(checksum1, checksum2, "Checksums should not match for 
different checksum types");
   }
+
+  @Test
+  public void testChecksumFromNonzeroPosition() throws Exception {
+    final ChunkBuffer data = mock(ChunkBuffer.class);
+    when(data.position()).thenReturn(Integer.MAX_VALUE - 1);
+    when(data.limit()).thenReturn(Integer.MAX_VALUE);
+    when(data.remaining()).thenReturn(1);
+    when(data.asByteBufferList()).thenReturn(
+        Collections.singletonList(ByteBuffer.wrap(new byte[] {1})));
+
+    final Checksum checksum = getChecksum(null, false);
+    assertEquals(1, checksum.computeChecksum(data).getChecksums().size());
+  }
 }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumCache.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumCache.java
index 0b7e9a7b198..0d0253c2130 100644
--- 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumCache.java
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumCache.java
@@ -18,10 +18,11 @@
 package org.apache.hadoop.ozone.common;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.function.Function;
 import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
 import org.apache.hadoop.ozone.common.Checksum.Algorithm;
+import org.apache.hadoop.ozone.common.Checksum.StreamingChecksum;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -33,7 +34,7 @@
 class TestChecksumCache {
 
   @ParameterizedTest
-  @EnumSource(ChecksumType.class)
+  @EnumSource(value = ChecksumType.class, names = {"CRC32", "CRC32C", 
"SHA256", "MD5"})
   void testComputeChecksum(ChecksumType checksumType) throws Exception {
     final int bytesPerChecksum = 16;
     ChecksumCache checksumCache = new ChecksumCache(bytesPerChecksum);
@@ -45,7 +46,7 @@ void testComputeChecksum(ChecksumType checksumType) throws 
Exception {
       byteArray[i] = (byte) (i % 128);
     }
 
-    final Function<ByteBuffer, ByteString> function = 
Algorithm.valueOf(checksumType).newChecksumFunction();
+    final StreamingChecksum algo = 
Algorithm.valueOf(checksumType).newStreamingChecksum();
 
     int iEnd = size / bytesPerChecksum + (size % bytesPerChecksum == 0 ? 0 : 
1);
     List<ByteString> lastRes = null;
@@ -54,19 +55,75 @@ void testComputeChecksum(ChecksumType checksumType) throws 
Exception {
       ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray, 0, byteBufferLength);
 
       try (ChunkBuffer chunkBuffer = 
ChunkBuffer.wrap(byteBuffer.asReadOnlyBuffer())) {
-        List<ByteString> res = checksumCache.computeChecksum(chunkBuffer, 
function);
-        System.out.println(res);
-        // Verify that every entry in the res list except the last one is the 
same as the one in lastRes list
+        List<ByteString> res = checksumCache.computeChecksum(chunkBuffer, 
algo, bytesPerChecksum);
+        // Every entry except the last must be unchanged from the prior 
iteration
+        // (those windows are fully cached and never re-computed).
         if (i > 0) {
           for (int j = 0; j < res.size() - 1; j++) {
             Assertions.assertEquals(lastRes.get(j), res.get(j));
           }
         }
-        lastRes = res;
+        lastRes = new ArrayList<>(res);
       }
     }
 
-    // Sanity check
     checksumCache.clear();
   }
+
+  @ParameterizedTest
+  @EnumSource(value = ChecksumType.class, names = {"CRC32", "CRC32C", 
"SHA256", "MD5"})
+  void testGrowingMultiBufferMatchesDirectChecksum(ChecksumType checksumType)
+      throws Exception {
+    final int bytesPerChecksum = 16;
+    final byte[] data = new byte[66];
+    for (int i = 0; i < data.length; i++) {
+      data[i] = (byte) i;
+    }
+
+    final Checksum cached = new Checksum(checksumType, bytesPerChecksum, true);
+    final Checksum direct = new Checksum(checksumType, bytesPerChecksum);
+    final int[] lengths = {1, 7, 15, 16, 17, 22, 31, 32, 33, 47, 48, 49, 65, 
66};
+    for (int length : lengths) {
+      final ChecksumData expected = direct.computeChecksum(
+          split(data, length, 7));
+      final ChecksumData actual = cached.computeChecksum(
+          split(data, length, 7), true);
+      Assertions.assertEquals(expected.getChecksums(), actual.getChecksums(),
+          "cached checksums must match direct checksums at length " + length);
+    }
+  }
+
+  @ParameterizedTest
+  @EnumSource(value = ChecksumType.class, names = {"CRC32", "CRC32C", 
"SHA256", "MD5"})
+  void testPartiallyPositionedMultiBufferMatchesDirectChecksum(
+      ChecksumType checksumType) throws Exception {
+    final int bytesPerChecksum = 4;
+    final byte[] data = new byte[11];
+    for (int i = 0; i < data.length; i++) {
+      data[i] = (byte) i;
+    }
+
+    final Checksum direct = new Checksum(checksumType, bytesPerChecksum);
+    final Checksum cached = new Checksum(checksumType, bytesPerChecksum, true);
+    final ChecksumData expected = 
direct.computeChecksum(partiallyPositioned(data));
+    final ChecksumData actual = 
cached.computeChecksum(partiallyPositioned(data), true);
+
+    Assertions.assertEquals(expected.getChecksums(), actual.getChecksums());
+  }
+
+  private static ChunkBuffer split(byte[] data, int length, int bufferSize) {
+    final List<ByteBuffer> buffers = new ArrayList<>();
+    for (int offset = 0; offset < length; offset += bufferSize) {
+      final int size = Math.min(bufferSize, length - offset);
+      buffers.add(ByteBuffer.wrap(data, offset, size).slice());
+    }
+    return ChunkBuffer.wrap(buffers);
+  }
+
+  private static ChunkBuffer partiallyPositioned(byte[] data) {
+    final List<ByteBuffer> buffers = new ArrayList<>();
+    buffers.add(ByteBuffer.wrap(data, 3, 4));
+    buffers.add(ByteBuffer.wrap(data, 7, 4).slice());
+    return ChunkBuffer.wrap(buffers);
+  }
 }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumMultiBuffer.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumMultiBuffer.java
new file mode 100644
index 00000000000..56a66387923
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksumMultiBuffer.java
@@ -0,0 +1,97 @@
+/*
+ * 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.hadoop.ozone.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+/**
+ * A checksum window that straddles multiple underlying buffers must
+ * produce the same checksum bytes as a single contiguous buffer over the
+ * same data.
+ */
+class TestChecksumMultiBuffer {
+
+  private static final int CHUNK_SIZE = 4 * 1024 * 1024;
+  private static final int BYTES_PER_CHECKSUM = 1024 * 1024;
+
+  @ParameterizedTest
+  @EnumSource(value = ChecksumType.class, names = {"CRC32", "CRC32C",
+      "SHA256", "MD5"})
+  void splitBufferProducesSameChecksumAsSingleBuffer(ChecksumType type)
+      throws Exception {
+    byte[] data = new byte[CHUNK_SIZE];
+    ThreadLocalRandom.current().nextBytes(data);
+
+    Checksum checksum = new Checksum(type, BYTES_PER_CHECKSUM);
+
+    // Single contiguous buffer.
+    ChecksumData single = checksum.computeChecksum(
+        ChunkBuffer.wrap(ByteBuffer.wrap(data.clone())));
+
+    // Split into 16 pieces of 256KB - each 1MB checksum window straddles
+    // exactly 4 underlying buffers.
+    int piece = 256 * 1024;
+    List<ByteBuffer> pieces = new ArrayList<>();
+    for (int off = 0; off < CHUNK_SIZE; off += piece) {
+      pieces.add(ByteBuffer.wrap(data, off, piece).slice());
+    }
+    ChecksumData split = checksum.computeChecksum(ChunkBuffer.wrap(pieces));
+
+    assertEquals(single.getChecksums().size(), split.getChecksums().size(),
+        "checksum count must match");
+    assertEquals(single.getChecksums(), split.getChecksums(),
+        "single-buffer and split-buffer must produce identical checksums "
+            + "for " + type);
+  }
+
+  @ParameterizedTest
+  @EnumSource(value = ChecksumType.class, names = {"CRC32", "CRC32C",
+      "SHA256", "MD5"})
+  void unalignedTrailingWindowIsHandled(ChecksumType type) throws Exception {
+    // Chunk size deliberately not a multiple of bytesPerChecksum, plus
+    // unaligned buffer splits, to exercise the trailing-partial-window
+    // path in computeChecksumDirect.
+    int total = BYTES_PER_CHECKSUM * 3 + 12345;
+    byte[] data = new byte[total];
+    ThreadLocalRandom.current().nextBytes(data);
+
+    Checksum checksum = new Checksum(type, BYTES_PER_CHECKSUM);
+
+    ChecksumData single = checksum.computeChecksum(
+        ChunkBuffer.wrap(ByteBuffer.wrap(data.clone())));
+
+    int piece = 333 * 1024; // intentionally awkward
+    List<ByteBuffer> pieces = new ArrayList<>();
+    for (int off = 0; off < total; off += piece) {
+      int len = Math.min(piece, total - off);
+      pieces.add(ByteBuffer.wrap(data, off, len).slice());
+    }
+    ChecksumData split = checksum.computeChecksum(ChunkBuffer.wrap(pieces));
+
+    assertEquals(single.getChecksums(), split.getChecksums(),
+        "unaligned split must match single-buffer for " + type);
+  }
+}
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/utils/TestBufferUtils.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/utils/TestBufferUtils.java
new file mode 100644
index 00000000000..3d5f091bc11
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/utils/TestBufferUtils.java
@@ -0,0 +1,81 @@
+/*
+ * 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.hadoop.ozone.common.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.ByteBuffer;
+import org.junit.jupiter.api.Test;
+
+class TestBufferUtils {
+
+  @Test
+  void slicePreservesSourceAndUsesRequestedRange() {
+    ByteBuffer source = ByteBuffer.allocate(16);
+    source.position(2);
+    source.limit(12);
+
+    ByteBuffer slice = BufferUtils.slice(source, 4, 5);
+
+    assertEquals(2, source.position());
+    assertEquals(12, source.limit());
+    assertEquals(4, slice.position());
+    assertEquals(9, slice.limit());
+  }
+
+  @Test
+  void sliceRejectsNegativePosition() {
+    IllegalArgumentException exception = assertThrows(
+        IllegalArgumentException.class,
+        () -> BufferUtils.slice(ByteBuffer.allocate(8), -1, 1));
+
+    assertEquals("position (-1) must not be negative", exception.getMessage());
+  }
+
+  @Test
+  void sliceRejectsNegativeLength() {
+    IllegalArgumentException exception = assertThrows(
+        IllegalArgumentException.class,
+        () -> BufferUtils.slice(ByteBuffer.allocate(8), 0, -1));
+
+    assertEquals("length (-1) must not be negative", exception.getMessage());
+  }
+
+  @Test
+  void sliceRejectsPositionBeyondLimit() {
+    IllegalArgumentException exception = assertThrows(
+        IllegalArgumentException.class,
+        () -> BufferUtils.slice(ByteBuffer.allocate(8), 9, 0));
+
+    assertEquals("position (9) exceeds source limit (8)",
+        exception.getMessage());
+  }
+
+  @Test
+  void sliceRejectsRangeBeyondLimitWithoutOverflow() {
+    IllegalArgumentException exception = assertThrows(
+        IllegalArgumentException.class,
+        () -> BufferUtils.slice(ByteBuffer.allocate(8), 1,
+            Integer.MAX_VALUE));
+
+    assertEquals(
+        "position (1) + length (2147483647) exceeds source limit (8)",
+        exception.getMessage());
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
index 1f8af28add1..a32a56a0267 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
@@ -1067,7 +1067,9 @@ private void 
validateChunkChecksumData(ChunkBufferToByteString data, ChunkInfo i
           final ChunkBuffer b = (ChunkBuffer)data;
           Checksum.verifyChecksum(b.duplicate(b.position(), b.limit()), 
info.getChecksumData(), 0);
         } else {
-          
Checksum.verifyChecksum(data.toByteString(byteBufferToByteString).asReadOnlyByteBuffer(),
+          // Skip concatenating into one ByteString - that would materialize
+          // a chunk-sized copy on the hot write path.
+          
Checksum.verifyChecksum(data.toByteStringList(byteBufferToByteString),
               info.getChecksumData(), 0);
         }
       } catch (OzoneChecksumException ex) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to