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

peterxcli 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 ae3358c0572 HDDS-13290. Correct the pagination semantic of 
listMultipartUploads (#8975)
ae3358c0572 is described below

commit ae3358c0572fbbed5b5ced6a533833c3d048a568
Author: Peter Lee <[email protected]>
AuthorDate: Mon Sep 1 15:46:12 2025 +0800

    HDDS-13290. Correct the pagination semantic of listMultipartUploads (#8975)
---
 .../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java   | 56 ++++++++++++++++++++++
 .../org/apache/hadoop/ozone/om/KeyManagerImpl.java | 12 ++---
 .../hadoop/ozone/om/OmMetadataManagerImpl.java     | 12 +++++
 3 files changed, 73 insertions(+), 7 deletions(-)

diff --git 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
index d3528eaaf7d..133bc031ab3 100644
--- 
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
+++ 
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java
@@ -815,6 +815,17 @@ public void testListMultipartUploadsPagination() {
       assertEquals(result.getKeyMarker(), keyMarker);
       assertEquals(result.getMaxUploads(), 10);
 
+      // Verify next markers content
+      if (result.isTruncated()) {
+        MultipartUpload lastUploadOnPage = result.getMultipartUploads()
+            .get(result.getMultipartUploads().size() - 1);
+        assertEquals(lastUploadOnPage.getKey(), result.getNextKeyMarker());
+        assertEquals(lastUploadOnPage.getUploadId(), 
result.getNextUploadIdMarker());
+      } else {
+        assertNull(result.getNextKeyMarker());
+        assertNull(result.getNextUploadIdMarker());
+      }
+
       // Update markers for next page
       keyMarker = result.getNextKeyMarker();
       uploadIdMarker = result.getNextUploadIdMarker();
@@ -851,6 +862,51 @@ public void testListMultipartUploadsPagination() {
             .collect(Collectors.toList()));
   }
 
+  @Test
+  public void testListMultipartUploadsPaginationCornerCases() {
+    final String bucketName = getBucketName();
+    final String keyA = getKeyName("samekey");
+    final String keyB = getKeyName("after");
+
+    s3Client.createBucket(bucketName);
+
+    // Create multiple MPUs for the same key to verify upload-id-marker 
semantics
+    List<String> keyAUploadIds = new ArrayList<>();
+    keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, 
null));
+    keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, 
null));
+    keyAUploadIds.add(initiateMultipartUpload(bucketName, keyA, null, null, 
null));
+    // Also create another key to ensure listing proceeds past keyA
+    initiateMultipartUpload(bucketName, keyB, null, null, null);
+
+    // Sort upload IDs lexicographically to match listing order for the same 
key
+    Collections.sort(keyAUploadIds);
+
+    // Case 1: key-marker=keyA and upload-id-marker set to the lowest uploadId
+    // Per spec, same-key uploads MAY be included if uploadId > marker
+    ListMultipartUploadsRequest request1 = new 
ListMultipartUploadsRequest(bucketName)
+        .withKeyMarker(keyA)
+        .withUploadIdMarker(keyAUploadIds.get(0))
+        .withMaxUploads(100);
+    MultipartUploadListing result1 = s3Client.listMultipartUploads(request1);
+
+    List<MultipartUpload> uploads1 = result1.getMultipartUploads();
+    // Collect same-key uploads and verify none are <= marker
+    List<String> sameKeyIds1 = uploads1.stream()
+        .filter(u -> keyA.equals(u.getKey()))
+        .map(MultipartUpload::getUploadId)
+        .collect(Collectors.toList());
+    assertThat(sameKeyIds1).allSatisfy(id -> 
assertTrue(id.compareTo(keyAUploadIds.get(0)) > 0));
+
+    // Case 2: key-marker=keyA and upload-id-marker set to the highest uploadId
+    // Expect no same-key (keyA) uploads to be returned
+    ListMultipartUploadsRequest request2 = new 
ListMultipartUploadsRequest(bucketName)
+        .withKeyMarker(keyA)
+        .withUploadIdMarker(keyAUploadIds.get(2))
+        .withMaxUploads(100);
+    MultipartUploadListing result2 = s3Client.listMultipartUploads(request2);
+    assertTrue(result2.getMultipartUploads().stream().noneMatch(u -> 
keyA.equals(u.getKey())));
+  }
+
   @Test
   public void testListParts(@TempDir Path tempDir) throws Exception {
     final String bucketName = getBucketName();
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
index 809974d9d7b..97af4c77af7 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java
@@ -986,14 +986,12 @@ public OmMultipartUploadList listMultipartUploads(String 
volumeName,
       OmMultipartUploadList.Builder resultBuilder = 
OmMultipartUploadList.newBuilder();
 
       if (withPagination && multipartUploadKeys.size() == maxUploads + 1) {
-        int lastIndex = multipartUploadKeys.size() - 1;
-        OmMultipartUpload lastUpload = multipartUploadKeys.get(lastIndex);
-        resultBuilder.setNextKeyMarker(lastUpload.getKeyName())
-            .setNextUploadIdMarker(lastUpload.getUploadId())
+        // Per spec, next markers should be the last element of the returned 
list, not the lookahead.
+        multipartUploadKeys.remove(multipartUploadKeys.size() - 1);
+        OmMultipartUpload lastReturned = 
multipartUploadKeys.get(multipartUploadKeys.size() - 1);
+        resultBuilder.setNextKeyMarker(lastReturned.getKeyName())
+            .setNextUploadIdMarker(lastReturned.getUploadId())
             .setIsTruncated(true);
-
-        // remove next upload from the list
-        multipartUploadKeys.remove(lastIndex);
       }
 
       return resultBuilder
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 04e0998219f..48988192b2d 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
@@ -17,6 +17,7 @@
 
 package org.apache.hadoop.ozone.om;
 
+import static 
org.apache.hadoop.hdds.StringUtils.getLexicographicallyHigherString;
 import static org.apache.hadoop.ozone.OzoneConsts.DB_TRANSIENT_MARKER;
 import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
 import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
@@ -1506,6 +1507,10 @@ public <KEY, VALUE> long 
countEstimatedRowsInTable(Table<KEY, VALUE> table)
     return count;
   }
 
+  /**
+   * Get the merged and paginated list of multipart upload keys from cache and 
DB.
+   * Only return at most maxUploads + 1 keys (lexicographical order).
+   */
   @Override
   public List<OmMultipartUpload> getMultipartUploadKeys(
       String volumeName, String bucketName, String prefix, String keyMarker,
@@ -1522,6 +1527,12 @@ public List<OmMultipartUpload> getMultipartUploadKeys(
       if (StringUtils.isNotBlank(uploadIdMarker)) {
         prefix = prefix + OM_KEY_PREFIX + uploadIdMarker;
       }
+      // - If upload-id-marker is not specified,
+      // only the keys lexicographically greater than the specified key-marker 
will be included in the list.
+      // - If upload-id-marker is specified,
+      // any multipart uploads for a key equal to the key-marker might also be 
included,
+      // provided those multipart uploads have upload IDs lexicographically 
greater than the specified upload-id-marker.
+      prefix = getLexicographicallyHigherString(prefix);
     }
     String seekKey = OmMultipartUpload.getDbKey(volumeName, bucketName, 
prefix);
 
@@ -1551,6 +1562,7 @@ public List<OmMultipartUpload> getMultipartUploadKeys(
         iterator = getMultipartInfoTable().iterator(prefixKey)) {
       iterator.seek(seekKey);
 
+      // Try to get maxUploads + 1 keys to check if the list is truncated.
       while (iterator.hasNext() && (noPagination || dbKeysCount < maxUploads + 
1)) {
         KeyValue<String, OmMultipartKeyInfo> entry = iterator.next();
         // If it is marked for abort, skip it.


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

Reply via email to