HDDS-13532. Refactor BucketEndpoint.get()#10045
Conversation
| // If you specify the encoding-type request parameter, should return encoded key name values | ||
| // in the following response elements: Delimiter, Prefix, Key, and StartAfter. | ||
| // For detail refer: | ||
| // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#AmazonS3-ListObjectsV2-response-EncodingType |
There was a problem hiding this comment.
nit : this comment is repeated.
| // Actual bucket processing starts here | ||
| // Validate and prepare parameters | ||
| BucketListingContext listingContext = validateAndPrepareParameters( | ||
| bucketName, delimiter, encodingType, marker, maxKeys, prefix, | ||
| continueToken, startAfter); | ||
|
|
||
| // If continuation token and start after both are provided, then we | ||
| // ignore start After | ||
| String prevKey = continueToken != null ? decodedToken.getLastKey() | ||
| : startAfter; | ||
| // Initialize response object | ||
| ListObjectResponse response = initializeListObjectResponse( | ||
| bucketName, delimiter, encodingType, marker, maxKeys, prefix, | ||
| continueToken, startAfter); |
There was a problem hiding this comment.
Here maxKeys is initialized from the query parameters (e.g., a user requests 5000), validateAndPrepareParameters is called. Inside this method, maxKeys is capped via validateMaxKeys(maxKeys) (e.g., capped to 1000). This capped value is stored inside the BucketListingContext, the maxKeys variable back in the get() method is not updated. It remains 5000. initializeListObjectResponse is called using the original maxKeys (5000).The response object now explicitly states 5000.
I think if we pass listingContext.getMaxKeys() not the local maxKeys to initializeListObjectResponse this issue would not be seen.
Could you confirm whether that’s intentional or something we should align?
|
@sreejasahithi Thanks for catching these! |
|
@rich7420 could you please resolve the conflicts in this branch. |
| @SuppressWarnings("parameternumber") | ||
| BucketListingContext(String bucketName, String delimiter, String encodingType, | ||
| String marker, int maxKeys, String prefix, String continueToken, | ||
| String startAfter, String prevKey, boolean shallow, | ||
| OzoneBucket bucket, Iterator<? extends OzoneKey> ozoneKeyIterator, | ||
| ContinueToken decodedToken) { |
There was a problem hiding this comment.
Instead of passing too many parameters, BucketListingContext should initialize itself directly from queryParams.
| @SuppressWarnings({"parameternumber", "checkstyle:ParameterNumber"}) | ||
| BucketListingContext validateAndPrepareParameters( | ||
| String bucketName, String delimiter, String encodingType, String marker, | ||
| int maxKeys, String prefix, String continueToken, String startAfter) | ||
| throws OS3Exception, IOException { |
There was a problem hiding this comment.
Please move validation into BucketListingContext, which should perform it on the values read from queryParams.
| * Context class to hold bucket listing parameters and state. | ||
| */ | ||
| static class BucketListingContext { |
There was a problem hiding this comment.
I'd prefer BucketListing as name, to not increase the number of "contexts" in S3 Gateway.
I think it can also be a top-level class.
| @SuppressWarnings({"parameternumber", "checkstyle:ParameterNumber"}) | ||
| ListObjectResponse initializeListObjectResponse( | ||
| String bucketName, String delimiter, String encodingType, String marker, | ||
| int maxKeys, String prefix, String continueToken, String startAfter) { |
There was a problem hiding this comment.
Please move the following methods into BucketListing(Context), and use member variables:
initializeListObjectResponseprocessKeyListingbuildFinalResponse
Also, please update the new unit test to exercise BucketListing only, without involving BucketEndpoint. (testGetDelegatesToAclHandler can be dropped.)
| public class BucketEndpoint extends BucketOperationHandler { | ||
| public class BucketEndpoint extends EndpointBase { |
There was a problem hiding this comment.
Please verify conflict resolution. The current state reverts ab71c6a.
|
@adoroszlai @sreejasahithi just try to refactor the changes |
adoroszlai
left a comment
There was a problem hiding this comment.
Thanks @rich7420 for improving the patch.
| @Override | ||
| Response handlePutRequest(S3RequestContext context, String bucketName, InputStream body) { | ||
| throw newError(S3ErrorTable.NOT_IMPLEMENTED, "PUT bucket"); | ||
| } | ||
|
|
There was a problem hiding this comment.
Please don't remove this.
| @Override | ||
| Response handleDeleteRequest(S3RequestContext context, String bucketName) { | ||
| throw newError(S3ErrorTable.NOT_IMPLEMENTED, "DELETE bucket"); | ||
| } | ||
|
|
There was a problem hiding this comment.
Please don't remove this.
There was a problem hiding this comment.
ok sorry about that
| OZONE_S3G_LIST_MAX_KEYS_LIMIT, | ||
| OZONE_S3G_LIST_MAX_KEYS_LIMIT_DEFAULT); | ||
|
|
||
| // initialize handlers |
There was a problem hiding this comment.
nit: unnecessary change
There was a problem hiding this comment.
ok sorry about that
| getMetrics().updateGetBucketFailureStats(context.getStartNanos()); | ||
| getMetrics().updateGetBucketFailureStats(startNanos); |
There was a problem hiding this comment.
nit: unnecessary change
| return BucketListing.fromQueryParams(this, bucketName) | ||
| .buildResponse(startNanos, perf); |
There was a problem hiding this comment.
Pass S3RequestContext context to BucketListing:
- use it to access
volume, which may be cached already - no need to pass individual parameters
startNanos, perf
| shallow = endpoint.isListKeysShallowEnabled() | ||
| && OZONE_URI_DELIMITER.equals(delimiter); | ||
|
|
||
| bucket = endpoint.getVolume().getBucket(bucketName); |
There was a problem hiding this comment.
context.getVolume() can be used here (after passing S3RequestContext)
| getMetrics().updateGetBucketFailureStats(context.getStartNanos()); | ||
| if (ex.getResult() == ResultCodes.FILE_NOT_FOUND) { | ||
| // File not found, continue and send normal response with 0 keyCount | ||
| LOG.debug("Key Not found prefix: {}", prefix); | ||
| } else { | ||
| throw ex; | ||
| getMetrics().updateGetBucketFailureStats(startNanos); | ||
| if (isAccessDenied(ex)) { | ||
| throw newError(S3ErrorTable.ACCESS_DENIED, bucketName, ex); | ||
| } | ||
| throw ex; |
There was a problem hiding this comment.
Seems like unintended change.
| } catch (OMException ex) { | ||
| getMetrics().updateGetBucketFailureStats(context.getStartNanos()); | ||
| if (ex.getResult() == ResultCodes.FILE_NOT_FOUND) { | ||
| // File not found, continue and send normal response with 0 keyCount | ||
| LOG.debug("Key Not found prefix: {}", prefix); | ||
| } else { | ||
| throw ex; | ||
| } | ||
| throw ex; |
There was a problem hiding this comment.
Since FILE_NOT_FOUND is handled within BucketListing, we don't need separate catch (OMException) anymore.
| bucket = context.getVolume().getBucket(bucketName); | ||
| S3Owner.verifyBucketOwnerCondition(endpoint.getHeaders(), bucketName, bucket.getOwner()); | ||
| try { | ||
| ozoneKeyIterator = bucket.listKeys(prefix, prevKey, shallow); |
There was a problem hiding this comment.
do we really need to call listKeys() in validateAndPrepare(), it seems like an expensive call where results are discared.
There was a problem hiding this comment.
I think ozoneKeyIterator is stored and consumed in processKeyListing(). also, listKeys() returns a lazy iterator no keys are fetched until the iterator is traversed.
| return Math.min(value, endpoint.getMaxKeysLimit()); | ||
| } | ||
|
|
||
| private ListObjectResponse initializeListObjectResponse() { |
There was a problem hiding this comment.
should this code be moved into builder?
There was a problem hiding this comment.
the function fromQueryParams() initializes all state,buildResponse() is the terminal step. I think adding a separate builder layer would increase complexity , isn't it?
5e21548 to
b2ad3f4
Compare
There was a problem hiding this comment.
Can you please add a class level javadoc here.
|
|
||
| if (count < maxKeys) { | ||
| response.setTruncated(false); | ||
| } else if (ozoneKeyIterator.hasNext() && lastKey != null) { |
There was a problem hiding this comment.
Better to put a null check on ozoneKeyIterator here.
| this.delimiter = endpoint.queryParams().get(QueryParams.DELIMITER); | ||
| this.encodingType = endpoint.queryParams().get(QueryParams.ENCODING_TYPE); | ||
| this.marker = endpoint.queryParams().get(QueryParams.MARKER); | ||
| this.maxKeys = endpoint.queryParams().getInt(QueryParams.MAX_KEYS, 1000); |
There was a problem hiding this comment.
Can we use OZONE_S3G_LIST_MAX_KEYS_LIMIT_DEFAULT here instead?
|
This PR has been marked as stale due to 21 days of inactivity. Please comment or remove the stale label to keep it open. Otherwise, it will be automatically closed in 7 days. |
Extract bucket listing logic from BucketEndpoint.handleGetRequest() into a top-level BucketListing class that initializes itself from queryParams, performs its own validation, and encapsulates initializeListObjectResponse, processKeyListing, and buildFinalResponse as member methods.
…guard ozoneKeyIterator
|
This PR has been marked as stale due to 21 days of inactivity. Please comment or remove the stale label to keep it open. Otherwise, it will be automatically closed in 7 days. |
|
Thank you for your contribution. This PR is being closed due to inactivity. Please contact a maintainer if you would like to reopen it. |
What changes were proposed in this pull request?
Reopen: #9110
This pull request refactors the massively monolithic
BucketEndpoint.get()method (ListObjects API implementation) to vastly improve codebase maintainability, readability, and unit-testability.The original
get()method was nearly 200 lines long. It tightly coupled logic across multiple domains—from parameter string deserialization, context variable instantiation, iterative key-prefix filtering logic, to S3 metrics/audit log emissions. This structure made it inherently risky to extend and nearly impossible to write isolated unit tests for individual components of the logic.Key Refactoring Improvements in this PR:
BucketListingContext): Introduced a dedicated package-private class to securely encapsulate listing state (such asbucketName,prefix,encodingType,maxKeys,decodedToken,ozoneKeyIterator, etc.). This successfully prevented "parameter-creep" and minimized variable clutter across the execution flow.validateAndPrepareParameters(): Sanitizes configuration limits, handles token decodings, verifies encoding types, and enforces S3 Bucket owner conditions.initializeListObjectResponse(): Generates the bare-bones boilerplate response structures.processKeyListing(): Owns the complex computational burden (thewhile (ozoneKeyIterator.hasNext())mechanism), resolving token depth matches, delimited deep structures, andCommonPrefixesallocations safely.buildFinalResponse(): Isolates the responsibility of executingPerformanceStringBuilderincrements and consistentauditReadSuccesspublishing.master's recent additions (S3RequestContext,PerformanceStringBuilder, and theBucketOperationHandlerChain-of-Responsibility pattern). The refactor cleanly embraces these integrations while achieving optimal flow footprint.What is the link to the Apache JIRA?
https://issues.apache.org/jira/browse/HDDS-13532
How was this patch tested?
https://github.com/rich7420/ozone/actions/runs/23993539910