Skip to content

HDDS-13532. Refactor BucketEndpoint.get()#10045

Closed
rich7420 wants to merge 2 commits into
apache:masterfrom
rich7420:HDDS-13532
Closed

HDDS-13532. Refactor BucketEndpoint.get()#10045
rich7420 wants to merge 2 commits into
apache:masterfrom
rich7420:HDDS-13532

Conversation

@rich7420

@rich7420 rich7420 commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

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:

  • Context Encapsulation (BucketListingContext): Introduced a dedicated package-private class to securely encapsulate listing state (such as bucketName, prefix, encodingType, maxKeys, decodedToken, ozoneKeyIterator, etc.). This successfully prevented "parameter-creep" and minimized variable clutter across the execution flow.
  • Method Extraction & Separation of Concerns: We systematically decomposed the unmaintainable block into multiple single-responsibility helper methods:
    • 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 (the while (ozoneKeyIterator.hasNext()) mechanism), resolving token depth matches, delimited deep structures, and CommonPrefixes allocations safely.
    • buildFinalResponse(): Isolates the responsibility of executing PerformanceStringBuilder increments and consistent auditReadSuccess publishing.
  • Seamless Upstream Synchronization: During refactoring, this branch was comprehensively synced and merged with master's recent additions (S3RequestContext, PerformanceStringBuilder, and the BucketOperationHandler Chain-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

@sreejasahithi sreejasahithi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rich7420

Comment on lines +292 to +295
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit : this comment is repeated.

Comment on lines +133 to +142
// 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);

@sreejasahithi sreejasahithi Apr 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@rich7420

rich7420 commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

@sreejasahithi Thanks for catching these!
I've removed the redundant comment and updated initializeListObjectResponse to properly use the capped listingContext.getMaxKeys(). The PR is updated.

@sarvekshayr
sarvekshayr self-requested a review April 22, 2026 05:10
@sreejasahithi

Copy link
Copy Markdown
Contributor

@rich7420 could you please resolve the conflicts in this branch.

Comment on lines +208 to +213
@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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing too many parameters, BucketListingContext should initialize itself directly from queryParams.

Comment on lines +286 to +290
@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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move validation into BucketListingContext, which should perform it on the values read from queryParams.

Comment on lines +191 to +193
* Context class to hold bucket listing parameters and state.
*/
static class BucketListingContext {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +335 to +338
@SuppressWarnings({"parameternumber", "checkstyle:ParameterNumber"})
ListObjectResponse initializeListObjectResponse(
String bucketName, String delimiter, String encodingType, String marker,
int maxKeys, String prefix, String continueToken, String startAfter) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the following methods into BucketListing(Context), and use member variables:

  • initializeListObjectResponse
  • processKeyListing
  • buildFinalResponse

Also, please update the new unit test to exercise BucketListing only, without involving BucketEndpoint. (testGetDelegatesToAclHandler can be dropped.)

@adoroszlai
adoroszlai marked this pull request as draft April 23, 2026 14:36
Comment on lines +75 to +77
public class BucketEndpoint extends BucketOperationHandler {
public class BucketEndpoint extends EndpointBase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please verify conflict resolution. The current state reverts ab71c6a.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adoroszlai thanks for the reminder!

@rich7420

Copy link
Copy Markdown
Contributor Author

@adoroszlai @sreejasahithi just try to refactor the changes
could you plz take a look to confirm the changes is right or not? thanks

@adoroszlai adoroszlai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rich7420 for improving the patch.

Comment on lines -276 to -280
@Override
Response handlePutRequest(S3RequestContext context, String bucketName, InputStream body) {
throw newError(S3ErrorTable.NOT_IMPLEMENTED, "PUT bucket");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove this.

Comment on lines -324 to -328
@Override
Response handleDeleteRequest(S3RequestContext context, String bucketName) {
throw newError(S3ErrorTable.NOT_IMPLEMENTED, "DELETE bucket");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sorry about that

OZONE_S3G_LIST_MAX_KEYS_LIMIT,
OZONE_S3G_LIST_MAX_KEYS_LIMIT_DEFAULT);

// initialize handlers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnecessary change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sorry about that

Comment on lines +151 to +115
getMetrics().updateGetBucketFailureStats(context.getStartNanos());
getMetrics().updateGetBucketFailureStats(startNanos);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnecessary change

Comment on lines +106 to +107
return BucketListing.fromQueryParams(this, bucketName)
.buildResponse(startNanos, perf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass S3RequestContext context to BucketListing:

  • use it to access volume, which may be cached already
  • no need to pass individual parameters startNanos, perf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it!

shallow = endpoint.isListKeysShallowEnabled()
&& OZONE_URI_DELIMITER.equals(delimiter);

bucket = endpoint.getVolume().getBucket(bucketName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.getVolume() can be used here (after passing S3RequestContext)

Comment on lines +143 to +113
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like unintended change.

Comment on lines +142 to +107
} 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since FILE_NOT_FOUND is handled within BucketListing, we don't need separate catch (OMException) anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

bucket = context.getVolume().getBucket(bucketName);
S3Owner.verifyBucketOwnerCondition(endpoint.getHeaders(), bucketName, bucket.getOwner());
try {
ozoneKeyIterator = bucket.listKeys(prefix, prevKey, shallow);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to call listKeys() in validateAndPrepare(), it seems like an expensive call where results are discared.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this code be moved into builder?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function fromQueryParams() initializes all state,buildResponse() is the terminal step. I think adding a separate builder layer would increase complexity , isn't it?

@rich7420
rich7420 force-pushed the HDDS-13532 branch 3 times, most recently from 5e21548 to b2ad3f4 Compare May 12, 2026 02:16
@adoroszlai
adoroszlai requested review from peterxcli and sreejasahithi and removed request for sreejasahithi May 16, 2026 17:10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a class level javadoc here.


if (count < maxKeys) {
response.setTruncated(false);
} else if (ozoneKeyIterator.hasNext() && lastKey != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use OZONE_S3G_LIST_MAX_KEYS_LIMIT_DEFAULT here instead?

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

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.

@github-actions github-actions Bot added the stale label Jun 8, 2026
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.
@github-actions github-actions Bot removed the stale label Jun 9, 2026
@github-actions

Copy link
Copy Markdown

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.

@github-actions github-actions Bot added the stale label Jun 30, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Thank you for your contribution. This PR is being closed due to inactivity. Please contact a maintainer if you would like to reopen it.

@github-actions github-actions Bot closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants