This is an automated email from the ASF dual-hosted git repository.
adoroszlai 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 fa8bd9dd70 HDDS-12916. Support ETag in listObjects response (#8356)
fa8bd9dd70 is described below
commit fa8bd9dd70fb1d22df390b1bd265b5751bd4f60d
Author: Ivan Andika <[email protected]>
AuthorDate: Sat May 3 19:45:04 2025 +0800
HDDS-12916. Support ETag in listObjects response (#8356)
---
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 9 +-
.../ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java | 152 ++++++++++++---------
.../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java | 133 +++++++++++++++++-
3 files changed, 226 insertions(+), 68 deletions(-)
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index f1e26ed87e..bcb08f0c3d 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -23,6 +23,7 @@
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_REQUIRED_OM_VERSION_MIN_KEY;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_SERVER_DEFAULTS_VALIDITY_PERIOD_MS;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_SERVER_DEFAULTS_VALIDITY_PERIOD_MS_DEFAULT;
+import static org.apache.hadoop.ozone.OzoneConsts.ETAG;
import static
org.apache.hadoop.ozone.OzoneConsts.MAXIMUM_NUMBER_OF_PARTS_PER_UPLOAD;
import static org.apache.hadoop.ozone.OzoneConsts.OLD_QUOTA_DEFAULT;
import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH;
@@ -1722,8 +1723,10 @@ public List<OzoneKey> listKeys(String volumeName, String
bucketName,
key.getCreationTime(),
key.getModificationTime(),
key.getReplicationConfig(),
+ Collections.singletonMap(ETAG, key.getETag()),
key.isFile(),
- key.getOwnerName()))
+ key.getOwnerName(),
+ Collections.emptyMap()))
.collect(Collectors.toList());
} else {
List<OmKeyInfo> keys = ozoneManagerClient.listKeys(
@@ -1735,8 +1738,10 @@ public List<OzoneKey> listKeys(String volumeName, String
bucketName,
key.getCreationTime(),
key.getModificationTime(),
key.getReplicationConfig(),
+ key.getMetadata(),
key.isFile(),
- key.getOwnerName()))
+ key.getOwnerName(),
+ key.getTags()))
.collect(Collectors.toList());
}
}
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 dc8f098286..cee69f0f36 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
@@ -98,7 +98,6 @@
import org.apache.hadoop.ozone.client.ObjectStore;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
-import org.apache.hadoop.ozone.client.OzoneClientFactory;
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
@@ -373,9 +372,8 @@ public void testPutDoubleSlashPrefixObject() throws
IOException {
final String bucketName = getBucketName();
final String keyName = "//dir1";
final String content = "bar";
- OzoneConfiguration conf = cluster.getConf();
// Create a FSO bucket for test
- try (OzoneClient ozoneClient = OzoneClientFactory.getRpcClient(conf)) {
+ try (OzoneClient ozoneClient = cluster.newClient()) {
ObjectStore store = ozoneClient.getObjectStore();
OzoneVolume volume = store.getS3Volume();
OmBucketInfo.Builder bucketInfo = new OmBucketInfo.Builder()
@@ -502,8 +500,7 @@ public void testGetObjectWithoutETag() throws Exception {
String value = "sample value";
byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
- OzoneConfiguration conf = cluster.getConf();
- try (OzoneClient ozoneClient = OzoneClientFactory.getRpcClient(conf)) {
+ try (OzoneClient ozoneClient = cluster.newClient()) {
ObjectStore store = ozoneClient.getObjectStore();
OzoneVolume volume = store.getS3Volume();
@@ -532,46 +529,16 @@ public void testGetObjectWithoutETag() throws Exception {
}
@Test
- public void testListObjectsMany() {
- final String bucketName = getBucketName();
- s3Client.createBucket(bucketName);
- final List<String> keyNames = Arrays.asList(
- getKeyName("1"),
- getKeyName("2"),
- getKeyName("3")
- );
-
- for (String keyName: keyNames) {
- s3Client.putObject(bucketName, keyName,
RandomStringUtils.secure().nextAlphanumeric(5));
- }
-
- ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
- .withBucketName(bucketName)
- .withMaxKeys(2);
- ObjectListing listObjectsResponse =
s3Client.listObjects(listObjectsRequest);
- assertThat(listObjectsResponse.getObjectSummaries()).hasSize(2);
- assertEquals(bucketName, listObjectsResponse.getBucketName());
- assertEquals(listObjectsResponse.getObjectSummaries().stream()
- .map(S3ObjectSummary::getKey).collect(Collectors.toList()),
- keyNames.subList(0, 2));
- assertTrue(listObjectsResponse.isTruncated());
-
-
- listObjectsRequest = new ListObjectsRequest()
- .withBucketName(bucketName)
- .withMaxKeys(2)
- .withMarker(listObjectsResponse.getNextMarker());
- listObjectsResponse = s3Client.listObjects(listObjectsRequest);
- assertThat(listObjectsResponse.getObjectSummaries()).hasSize(1);
- assertEquals(bucketName, listObjectsResponse.getBucketName());
- assertEquals(listObjectsResponse.getObjectSummaries().stream()
- .map(S3ObjectSummary::getKey).collect(Collectors.toList()),
- keyNames.subList(2, keyNames.size()));
- assertFalse(listObjectsResponse.isTruncated());
+ public void testListObjectsMany() throws Exception {
+ testListObjectsMany(false);
}
@Test
- public void testListObjectsManyV2() {
+ public void testListObjectsManyV2() throws Exception {
+ testListObjectsMany(true);
+ }
+
+ private void testListObjectsMany(boolean isListV2) throws Exception {
final String bucketName = getBucketName();
s3Client.createBucket(bucketName);
final List<String> keyNames = Arrays.asList(
@@ -579,34 +546,91 @@ public void testListObjectsManyV2() {
getKeyName("2"),
getKeyName("3")
);
+ final List<String> keyNamesWithoutETag = Arrays.asList(
+ getKeyName("4"),
+ getKeyName("5")
+ );
+ final Map<String, String> keyToEtag = new HashMap<>();
for (String keyName: keyNames) {
- s3Client.putObject(bucketName, keyName,
RandomStringUtils.secure().nextAlphanumeric(5));
+ PutObjectResult putObjectResult = s3Client.putObject(bucketName, keyName,
+ RandomStringUtils.secure().nextAlphanumeric(5));
+ keyToEtag.put(keyName, putObjectResult.getETag());
}
+ try (OzoneClient ozoneClient = cluster.newClient()) {
+ ObjectStore store = ozoneClient.getObjectStore();
- ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
- .withBucketName(bucketName)
- .withMaxKeys(2);
- ListObjectsV2Result listObjectsResponse =
s3Client.listObjectsV2(listObjectsRequest);
- assertThat(listObjectsResponse.getObjectSummaries()).hasSize(2);
- assertEquals(bucketName, listObjectsResponse.getBucketName());
- assertEquals(listObjectsResponse.getObjectSummaries().stream()
+ OzoneVolume volume = store.getS3Volume();
+ OzoneBucket bucket = volume.getBucket(bucketName);
+
+ for (String keyNameWithoutETag : keyNamesWithoutETag) {
+ byte[] valueBytes =
RandomStringUtils.secure().nextAlphanumeric(5).getBytes(StandardCharsets.UTF_8);
+ try (OzoneOutputStream out = bucket.createKey(keyNameWithoutETag,
+ valueBytes.length,
+ ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS,
ReplicationFactor.ONE),
+ Collections.emptyMap())) {
+ out.write(valueBytes);
+ }
+ }
+ }
+
+ List<S3ObjectSummary> objectSummaries;
+ String continuationToken;
+ if (isListV2) {
+ ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
+ .withBucketName(bucketName)
+ .withMaxKeys(2);
+ ListObjectsV2Result listObjectsResponse =
s3Client.listObjectsV2(listObjectsRequest);
+ objectSummaries = listObjectsResponse.getObjectSummaries();
+ assertEquals(bucketName, listObjectsResponse.getBucketName());
+ assertTrue(listObjectsResponse.isTruncated());
+ continuationToken = listObjectsResponse.getNextContinuationToken();
+ } else {
+ ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+ .withBucketName(bucketName)
+ .withMaxKeys(2);
+ ObjectListing listObjectsResponse =
s3Client.listObjects(listObjectsRequest);
+ objectSummaries = listObjectsResponse.getObjectSummaries();
+ assertEquals(bucketName, listObjectsResponse.getBucketName());
+ assertTrue(listObjectsResponse.isTruncated());
+ continuationToken = listObjectsResponse.getNextMarker();
+ }
+ assertThat(objectSummaries).hasSize(2);
+ assertEquals(objectSummaries.stream()
.map(S3ObjectSummary::getKey).collect(Collectors.toList()),
keyNames.subList(0, 2));
- assertTrue(listObjectsResponse.isTruncated());
+ for (S3ObjectSummary objectSummary : objectSummaries) {
+ assertEquals(keyToEtag.get(objectSummary.getKey()),
objectSummary.getETag());
+ }
+ // Include both keys with and without ETag
+ if (isListV2) {
+ ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
+ .withBucketName(bucketName)
+ .withMaxKeys(5)
+ .withContinuationToken(continuationToken);
+ ListObjectsV2Result listObjectsResponse =
s3Client.listObjectsV2(listObjectsRequest);
+ objectSummaries = listObjectsResponse.getObjectSummaries();
+ assertEquals(bucketName, listObjectsResponse.getBucketName());
+ assertFalse(listObjectsResponse.isTruncated());
+ } else {
+ ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+ .withBucketName(bucketName)
+ .withMaxKeys(5)
+ .withMarker(continuationToken);
+ ObjectListing listObjectsResponse =
s3Client.listObjects(listObjectsRequest);
+ objectSummaries = listObjectsResponse.getObjectSummaries();
+ assertEquals(bucketName, listObjectsResponse.getBucketName());
+ assertFalse(listObjectsResponse.isTruncated());
+ }
- listObjectsRequest = new ListObjectsV2Request()
- .withBucketName(bucketName)
- .withMaxKeys(2)
- .withContinuationToken(listObjectsResponse.getNextContinuationToken());
- listObjectsResponse = s3Client.listObjectsV2(listObjectsRequest);
- assertThat(listObjectsResponse.getObjectSummaries()).hasSize(1);
- assertEquals(bucketName, listObjectsResponse.getBucketName());
- assertEquals(listObjectsResponse.getObjectSummaries().stream()
- .map(S3ObjectSummary::getKey).collect(Collectors.toList()),
- keyNames.subList(2, keyNames.size()));
- assertFalse(listObjectsResponse.isTruncated());
+ assertThat(objectSummaries).hasSize(3);
+ assertEquals(keyNames.get(2), objectSummaries.get(0).getKey());
+ assertEquals(keyNamesWithoutETag.get(0), objectSummaries.get(1).getKey());
+ assertEquals(keyNamesWithoutETag.get(1), objectSummaries.get(2).getKey());
+ for (S3ObjectSummary objectSummary : objectSummaries) {
+ assertEquals(keyToEtag.get(objectSummary.getKey()),
objectSummary.getETag());
+ }
}
@Test
@@ -961,7 +985,7 @@ private boolean isBucketEmpty(Bucket bucket) {
}
private String getBucketName() {
- return getBucketName(null);
+ return getBucketName("");
}
private String getBucketName(String suffix) {
@@ -969,7 +993,7 @@ private String getBucketName(String suffix) {
}
private String getKeyName() {
- return getKeyName(null);
+ return getKeyName("");
}
private String getKeyName(String suffix) {
diff --git
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
index 834580b6e8..7ef1342886 100644
---
a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
+++
b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java
@@ -20,7 +20,9 @@
import static org.apache.hadoop.ozone.OzoneConsts.MB;
import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.calculateDigest;
import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
@@ -28,17 +30,29 @@
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.stream.Collectors;
import javax.xml.bind.DatatypeConverter;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationFactor;
+import org.apache.hadoop.hdds.client.ReplicationType;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
import org.apache.hadoop.ozone.s3.S3ClientFactory;
import org.apache.hadoop.ozone.s3.S3GatewayService;
import org.apache.ozone.test.OzoneTestBase;
@@ -57,7 +71,12 @@
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
+import software.amazon.awssdk.services.s3.model.ListObjectsRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Object;
import software.amazon.awssdk.services.s3.model.Tag;
import software.amazon.awssdk.services.s3.model.Tagging;
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
@@ -133,6 +152,116 @@ public void testPutObject() {
assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"",
getObjectResponse.eTag());
}
+ @Test
+ public void testListObjectsMany() throws Exception {
+ testListObjectsMany(false);
+ }
+
+ @Test
+ public void testListObjectsManyV2() throws Exception {
+ testListObjectsMany(true);
+ }
+
+ private void testListObjectsMany(boolean isListV2) throws Exception {
+ final String bucketName = getBucketName();
+ s3Client.createBucket(b -> b.bucket(bucketName));
+ final List<String> keyNames = Arrays.asList(
+ getKeyName("1"),
+ getKeyName("2"),
+ getKeyName("3")
+ );
+ final List<String> keyNamesWithoutETag = Arrays.asList(
+ getKeyName("4"),
+ getKeyName("5")
+ );
+ final Map<String, String> keyToEtag = new HashMap<>();
+ for (String keyName: keyNames) {
+ PutObjectResponse putObjectResponse = s3Client.putObject(b -> b
+ .bucket(bucketName)
+ .key(keyName),
+
RequestBody.fromString(RandomStringUtils.secure().nextAlphanumeric(5)));
+ keyToEtag.put(keyName, putObjectResponse.eTag());
+ }
+ try (OzoneClient ozoneClient = cluster.newClient()) {
+ ObjectStore store = ozoneClient.getObjectStore();
+
+ OzoneVolume volume = store.getS3Volume();
+ OzoneBucket bucket = volume.getBucket(bucketName);
+
+ for (String keyNameWithoutETag : keyNamesWithoutETag) {
+ byte[] valueBytes =
RandomStringUtils.secure().nextAlphanumeric(5).getBytes(StandardCharsets.UTF_8);
+ try (OzoneOutputStream out = bucket.createKey(keyNameWithoutETag,
+ valueBytes.length,
+ ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS,
ReplicationFactor.ONE),
+ Collections.emptyMap())) {
+ out.write(valueBytes);
+ }
+ }
+ }
+
+ List<S3Object> s3Objects;
+ String continuationToken;
+ if (isListV2) {
+ ListObjectsV2Request listObjectsRequest = ListObjectsV2Request.builder()
+ .bucket(bucketName)
+ .maxKeys(2)
+ .build();
+ ListObjectsV2Response listObjectsResponse =
s3Client.listObjectsV2(listObjectsRequest);
+ s3Objects = listObjectsResponse.contents();
+ assertEquals(bucketName, listObjectsResponse.name());
+ assertTrue(listObjectsResponse.isTruncated());
+ continuationToken = listObjectsResponse.nextContinuationToken();
+ } else {
+ ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder()
+ .bucket(bucketName)
+ .maxKeys(2)
+ .build();
+ ListObjectsResponse listObjectsResponse =
s3Client.listObjects(listObjectsRequest);
+ s3Objects = listObjectsResponse.contents();
+ assertEquals(bucketName, listObjectsResponse.name());
+ assertTrue(listObjectsResponse.isTruncated());
+ continuationToken = listObjectsResponse.nextMarker();
+ }
+ assertThat(s3Objects).hasSize(2);
+ assertEquals(s3Objects.stream()
+ .map(S3Object::key).collect(Collectors.toList()),
+ keyNames.subList(0, 2));
+ for (S3Object s3Object : s3Objects) {
+ assertEquals(keyToEtag.get(s3Object.key()), s3Object.eTag());
+ }
+
+ // Include both keys with and without ETag
+ if (isListV2) {
+ ListObjectsV2Request listObjectsRequest = ListObjectsV2Request.builder()
+ .bucket(bucketName)
+ .maxKeys(5)
+ .continuationToken(continuationToken)
+ .build();
+ ListObjectsV2Response listObjectsResponse =
s3Client.listObjectsV2(listObjectsRequest);
+ s3Objects = listObjectsResponse.contents();
+ assertEquals(bucketName, listObjectsResponse.name());
+ assertFalse(listObjectsResponse.isTruncated());
+ } else {
+ ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder()
+ .bucket(bucketName)
+ .maxKeys(5)
+ .marker(continuationToken)
+ .build();
+ ListObjectsResponse listObjectsResponse =
s3Client.listObjects(listObjectsRequest);
+ s3Objects = listObjectsResponse.contents();
+ assertEquals(bucketName, listObjectsResponse.name());
+ assertFalse(listObjectsResponse.isTruncated());
+ }
+
+ assertThat(s3Objects).hasSize(3);
+ assertEquals(keyNames.get(2), s3Objects.get(0).key());
+ assertEquals(keyNamesWithoutETag.get(0), s3Objects.get(1).key());
+ assertEquals(keyNamesWithoutETag.get(1), s3Objects.get(2).key());
+ for (S3Object s3Object : s3Objects) {
+ assertEquals(keyToEtag.get(s3Object.key()), s3Object.eTag());
+ }
+ }
+
@Test
public void testCopyObject() {
final String sourceBucketName = getBucketName("source");
@@ -196,7 +325,7 @@ public void testLowLevelMultipartUpload(@TempDir Path
tempDir) throws Exception
}
private String getBucketName() {
- return getBucketName(null);
+ return getBucketName("");
}
private String getBucketName(String suffix) {
@@ -204,7 +333,7 @@ private String getBucketName(String suffix) {
}
private String getKeyName() {
- return getKeyName(null);
+ return getKeyName("");
}
private String getKeyName(String suffix) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]