ArafatKhan2198 commented on code in PR #11020:
URL: https://github.com/apache/ozone/pull/11020#discussion_r3878720533


##########
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java:
##########
@@ -170,4 +173,89 @@ private GetObjectAttributesResponse buildResponse(OzoneKey 
key, Set<String> requ
 
     return resp;
   }
+
+  /**
+   * Builds the {@link GetObjectAttributesResponse.ObjectParts} element for a 
completed
+   * multipart object, including per-part sizes and optional pagination.
+   *
+   * <p>When {@code x-amz-max-parts} is omitted, the page size defaults to 
1000, matching ListParts.
+   * Each part size is fetched via a part-aware {@code headS3Object} call (one 
OM RPC per part
+   * in the current page).
+   */
+  private GetObjectAttributesResponse.ObjectParts buildObjectParts(String 
bucketName,
+      String keyPath, int totalPartsCount, String resource) throws 
IOException, OS3Exception {
+    Integer maxPartsHeader = parseMaxPartsHeader(resource);
+    Integer partNumberMarker = parsePartNumberMarkerHeader(resource);
+    int marker = partNumberMarker != null ? partNumberMarker : 0;
+    int maxParts = maxPartsHeader != null
+        ? maxPartsHeader : GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT;
+
+    GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
+    parts.setPartsCount(totalPartsCount);
+    parts.setMaxParts(maxParts);
+    if (partNumberMarker != null) {
+      parts.setPartNumberMarker(partNumberMarker);
+    }
+
+    int lastPartReturned = marker;
+    for (int partNumber = marker + 1;
+         partNumber <= totalPartsCount && parts.getParts().size() < maxParts;
+         partNumber++) {
+      OzoneKey partKey;
+      try {
+        partKey = getClientProtocol().headS3Object(bucketName, keyPath, 
partNumber);

Review Comment:
   This implementation sends one OM request for every part. With the default 
`max-parts` value of 1000, one `GetObjectAttributes` request can result in 
around 1001 sequential OM calls, including the initial object lookup.
   
   This may cause high latency in S3 Gateway and heavy load on OM. It can also 
return inconsistent information if the object is overwritten while these calls 
are running.
   
   Can we provide one bulk or paginated OM operation that returns the part 
number and size for all parts in the requested page?
   



##########
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java:
##########
@@ -170,4 +173,89 @@ private GetObjectAttributesResponse buildResponse(OzoneKey 
key, Set<String> requ
 
     return resp;
   }
+
+  /**
+   * Builds the {@link GetObjectAttributesResponse.ObjectParts} element for a 
completed
+   * multipart object, including per-part sizes and optional pagination.
+   *
+   * <p>When {@code x-amz-max-parts} is omitted, the page size defaults to 
1000, matching ListParts.
+   * Each part size is fetched via a part-aware {@code headS3Object} call (one 
OM RPC per part
+   * in the current page).
+   */
+  private GetObjectAttributesResponse.ObjectParts buildObjectParts(String 
bucketName,
+      String keyPath, int totalPartsCount, String resource) throws 
IOException, OS3Exception {
+    Integer maxPartsHeader = parseMaxPartsHeader(resource);
+    Integer partNumberMarker = parsePartNumberMarkerHeader(resource);
+    int marker = partNumberMarker != null ? partNumberMarker : 0;
+    int maxParts = maxPartsHeader != null
+        ? maxPartsHeader : GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT;
+
+    GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
+    parts.setPartsCount(totalPartsCount);
+    parts.setMaxParts(maxParts);
+    if (partNumberMarker != null) {
+      parts.setPartNumberMarker(partNumberMarker);
+    }
+
+    int lastPartReturned = marker;
+    for (int partNumber = marker + 1;
+         partNumber <= totalPartsCount && parts.getParts().size() < maxParts;
+         partNumber++) {
+      OzoneKey partKey;
+      try {
+        partKey = getClientProtocol().headS3Object(bucketName, keyPath, 
partNumber);
+      } catch (OMException ex) {
+        if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
+          throw newError(NO_SUCH_KEY, keyPath, ex);
+        } else if (isAccessDenied(ex)) {
+          throw newError(ACCESS_DENIED, bucketName + "/" + keyPath, ex);
+        }
+        throw newError(resource, ex);
+      }
+      parts.addPart(new GetObjectAttributesResponse.Part(partNumber, 
partKey.getDataSize()));
+      lastPartReturned = partNumber;
+    }
+
+    boolean truncated = lastPartReturned < totalPartsCount;
+    parts.setTruncated(truncated);
+    if (truncated) {
+      parts.setNextPartNumberMarker(lastPartReturned);
+    }
+    return parts;
+  }
+
+  private Integer parseMaxPartsHeader(String resource) throws OS3Exception {
+    String headerValue = getHeaders().getHeaderString(MAX_PARTS_HEADER);
+    if (StringUtils.isBlank(headerValue)) {
+      return null;
+    }
+    try {
+      int maxParts = Integer.parseInt(headerValue.trim());
+      if (maxParts <= 0 || maxParts > GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT) {
+        throw newError(INVALID_ARGUMENT, resource,
+            new IllegalArgumentException("max-parts must be between 1 and "
+                + GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT));
+      }
+      return maxParts;
+    } catch (NumberFormatException ex) {
+      throw newError(INVALID_ARGUMENT, resource, ex);
+    }
+  }
+
+  private Integer parsePartNumberMarkerHeader(String resource) throws 
OS3Exception {
+    String headerValue = 
getHeaders().getHeaderString(PART_NUMBER_MARKER_HEADER);
+    if (StringUtils.isBlank(headerValue)) {
+      return null;
+    }
+    try {
+      int marker = Integer.parseInt(headerValue.trim());
+      if (marker < 0) {

Review Comment:
   The marker currently has no upper limit. If the client sends 
`Integer.MAX_VALUE`, the expression `marker + 1` will overflow and become a 
negative number.
   
   This may cause negative part numbers or incorrect part sizes in the response.
   
   Can we validate that `part-number-marker` is between 0 and 10,000 before 
using it? Please also add tests for 10,000, 10,001, and `Integer.MAX_VALUE`.
   



##########
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java:
##########
@@ -170,4 +173,89 @@ private GetObjectAttributesResponse buildResponse(OzoneKey 
key, Set<String> requ
 
     return resp;
   }
+
+  /**
+   * Builds the {@link GetObjectAttributesResponse.ObjectParts} element for a 
completed
+   * multipart object, including per-part sizes and optional pagination.
+   *
+   * <p>When {@code x-amz-max-parts} is omitted, the page size defaults to 
1000, matching ListParts.
+   * Each part size is fetched via a part-aware {@code headS3Object} call (one 
OM RPC per part
+   * in the current page).
+   */
+  private GetObjectAttributesResponse.ObjectParts buildObjectParts(String 
bucketName,
+      String keyPath, int totalPartsCount, String resource) throws 
IOException, OS3Exception {
+    Integer maxPartsHeader = parseMaxPartsHeader(resource);
+    Integer partNumberMarker = parsePartNumberMarkerHeader(resource);
+    int marker = partNumberMarker != null ? partNumberMarker : 0;
+    int maxParts = maxPartsHeader != null
+        ? maxPartsHeader : GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT;
+
+    GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
+    parts.setPartsCount(totalPartsCount);
+    parts.setMaxParts(maxParts);
+    if (partNumberMarker != null) {
+      parts.setPartNumberMarker(partNumberMarker);
+    }
+
+    int lastPartReturned = marker;
+    for (int partNumber = marker + 1;

Review Comment:
   I think there is a correctness issue here. `totalPartsCount` tells us how 
many parts exist, but it does not tell us the highest part number.
   
   Ozone supports completed multipart objects with non-contiguous part numbers, 
for example parts 1 and 3. In that case, the total part count is 2, but this 
loop tries to fetch parts 1 and 2. Part 2 will fail with `InvalidPart`, and 
part 3 will never be returned.
   
   Can we fetch the actual completed part numbers from OM and paginate over 
those part numbers instead? Please also add a test using non-contiguous parts 
such as 1 and 3.
   



##########
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java:
##########
@@ -170,4 +173,89 @@ private GetObjectAttributesResponse buildResponse(OzoneKey 
key, Set<String> requ
 
     return resp;
   }
+
+  /**
+   * Builds the {@link GetObjectAttributesResponse.ObjectParts} element for a 
completed
+   * multipart object, including per-part sizes and optional pagination.
+   *
+   * <p>When {@code x-amz-max-parts} is omitted, the page size defaults to 
1000, matching ListParts.
+   * Each part size is fetched via a part-aware {@code headS3Object} call (one 
OM RPC per part
+   * in the current page).
+   */
+  private GetObjectAttributesResponse.ObjectParts buildObjectParts(String 
bucketName,
+      String keyPath, int totalPartsCount, String resource) throws 
IOException, OS3Exception {
+    Integer maxPartsHeader = parseMaxPartsHeader(resource);
+    Integer partNumberMarker = parsePartNumberMarkerHeader(resource);
+    int marker = partNumberMarker != null ? partNumberMarker : 0;
+    int maxParts = maxPartsHeader != null
+        ? maxPartsHeader : GET_OBJECT_ATTRIBUTES_MAX_PARTS_LIMIT;
+
+    GetObjectAttributesResponse.ObjectParts parts = new 
GetObjectAttributesResponse.ObjectParts();
+    parts.setPartsCount(totalPartsCount);
+    parts.setMaxParts(maxParts);
+    if (partNumberMarker != null) {
+      parts.setPartNumberMarker(partNumberMarker);
+    }
+
+    int lastPartReturned = marker;
+    for (int partNumber = marker + 1;
+         partNumber <= totalPartsCount && parts.getParts().size() < maxParts;
+         partNumber++) {
+      OzoneKey partKey;
+      try {
+        partKey = getClientProtocol().headS3Object(bucketName, keyPath, 
partNumber);
+      } catch (OMException ex) {
+        if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
+          throw newError(NO_SUCH_KEY, keyPath, ex);
+        } else if (isAccessDenied(ex)) {
+          throw newError(ACCESS_DENIED, bucketName + "/" + keyPath, ex);
+        }
+        throw newError(resource, ex);
+      }
+      parts.addPart(new GetObjectAttributesResponse.Part(partNumber, 
partKey.getDataSize()));

Review Comment:
   Could you please confirm the expected AWS compatibility here?
   
   AWS documentation says that for general-purpose buckets, the `Part` elements 
are not returned when the object does not have an additional checksum. Ozone 
currently does not store these additional checksums, but this implementation 
always returns the part entries.
   
   If this is an intentional Ozone difference, we should document it. 
Otherwise, the part entries may need to be returned only when the required 
checksum information is available.
   



-- 
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]


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

Reply via email to