Skip to content

ACC-3102 Switch from minio-java-sdk for s3 to aws-sdk v2 - #335

Open
michyspiri wants to merge 5 commits into
mainfrom
ACC-3102
Open

ACC-3102 Switch from minio-java-sdk for s3 to aws-sdk v2#335
michyspiri wants to merge 5 commits into
mainfrom
ACC-3102

Conversation

@michyspiri

Copy link
Copy Markdown
Contributor

No description provided.

@michyspiri
michyspiri requested a review from a team as a code owner July 31, 2026 08:47
S3Client s3Client(S3Properties properties) {
// connection-pool-size has no equivalent in the Apache http client: it was the maximum number of *idle*
// connections OkHttp kept around (0 by default: no connection reuse, cfr. ACC-2697). The same net effect
// is preserved by evicting idle connections after the keep-alive period.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Having a short keepAlive isn't the same as not pooling connections. Also, the git commit on which the comment is based mentions the wrong ticket number, the connection re-use ticket was https://xenitsupport.jira.com/browse/ACC-2696.
Can you try setting the HttpHeader Connection to close? That should make the client not re-use the connections.

import software.amazon.awssdk.services.s3.S3Client;

/**
* Creates {@link S3Client} instances configured the way the previously used minio client was, so that the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Referencing the "previously used minio client" everywhere is not useful. We don't care about it being configured exactly the same, as long as it works the same

* <li>Path-style access is used unless the endpoint is an AWS endpoint (minio derived this the same way).
* The AWS SDK would otherwise default to virtual-host style, which most S3-compatible stores do not
* support.</li>
* <li>Without an access key + secret key pair, requests are sent unsigned (anonymous), like minio did.</li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have never used anonymous access

* Creates {@link S3Client} instances configured the way the previously used minio client was, so that the
* existing configuration properties keep their exact semantics after the switch to the AWS SDK:
* <ul>
* <li>An endpoint without scheme gets {@code https://} prepended, and a path in the endpoint is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The endpoint is always supplied with a protocol, so this prefixing is useless

* rejected.</li>
* <li>Path-style access is used unless the endpoint is an AWS endpoint (minio derived this the same way).
* The AWS SDK would otherwise default to virtual-host style, which most S3-compatible stores do not
* support.</li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minio did not make path-style access configurable.

In the past (in the spring-content based system), we had a configuration parameter to enable/disable path-style access. We can make that explicitly configurable again. Because maybe some systems other than AWS don't support path-style access either.

* The AWS SDK would otherwise default to virtual-host style, which most S3-compatible stores do not
* support.</li>
* <li>Without an access key + secret key pair, requests are sent unsigned (anonymous), like minio did.</li>
* <li>Without a region, {@code us-east-1} is used. minio would look up the bucket region with a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of setting the region to an existing AWS region (which can be confusing when forgetting to supply a region), configure it to none instead.

Comment on lines +31 to +33
* <li>The default profile file is replaced with an empty one, so the SDK never reads {@code ~/.aws/config}
* or {@code ~/.aws/credentials}. minio never read those files, and a broken file there (e.g. written by a
* newer aws CLI) would otherwise break client construction.</li>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A broken file is not really the reason why we don't want it to read the profile file.

We don't want to read that, because when running it for local development, we don't want it to pick up any potential AWS configuration and credentials that are present on the user account. That may result in accidentally connecting to some production environment, which we certainly don't want.

* or {@code ~/.aws/credentials}. minio never read those files, and a broken file there (e.g. written by a
* newer aws CLI) would otherwise break client construction.</li>
* </ul>
* Internal to the appserver auto-configuration; not intended as public API.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then make it package-private

implementation project(':contentgrid-appserver-blueprintartifact-impl-utils')
implementation 'io.minio:minio'
implementation('software.amazon.awssdk:s3') {
// Only the synchronous ApacheHttpClient is used; keep the Netty-based async client off the classpath

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we explicilty choosing a different option than the supplied default?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@michyspiri Did you miss this question?

Comment on lines +105 to +178
private void writeObject(ContentReference contentReference, InputStream inputStream) throws IOException {
// readNBytes allocates proportionally to the stream size, so small objects don't cost a full part buffer
var firstPart = inputStream.readNBytes(PART_SIZE);
if (firstPart.length < PART_SIZE) {
client.putObject(PutObjectRequest.builder()
.bucket(bucketName)
.key(contentReference.getValue())
.contentType(CONTENT_TYPE)
.build(),
requestBodyFor(firstPart, firstPart.length));
} else {
writeObjectMultipart(contentReference, inputStream, firstPart);
}
}

/**
* @param buffer completely filled with the first part; reused as read buffer for every following part
*/
private void writeObjectMultipart(ContentReference contentReference, InputStream inputStream, byte[] buffer)
throws IOException {
var uploadId = client.createMultipartUpload(CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(contentReference.getValue())
.contentType(CONTENT_TYPE)
.build())
.uploadId();
try {
var completedParts = new ArrayList<CompletedPart>();
var bytesInBuffer = buffer.length;
while (bytesInBuffer > 0) {
var partNumber = completedParts.size() + 1;
var uploadPartResponse = client.uploadPart(UploadPartRequest.builder()
.bucket(bucketName)
.key(contentReference.getValue())
.uploadId(uploadId)
.partNumber(partNumber)
.build(),
requestBodyFor(buffer, bytesInBuffer));
completedParts.add(CompletedPart.builder()
.partNumber(partNumber)
.eTag(uploadPartResponse.eTag())
.build());
if (bytesInBuffer < PART_SIZE) {
break; // The stream ended inside this part, so it was the last one
}
bytesInBuffer = inputStream.readNBytes(buffer, 0, buffer.length);
}
client.completeMultipartUpload(CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(contentReference.getValue())
.uploadId(uploadId)
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
.build());
} catch (SdkException | IOException e) {
abortMultipartUpload(contentReference, uploadId, e);
throw e;
}
}

private void abortMultipartUpload(ContentReference contentReference, String uploadId, Exception cause) {
try {
client.abortMultipartUpload(AbortMultipartUploadRequest.builder()
.bucket(bucketName)
.key(contentReference.getValue())
.uploadId(uploadId)
.build());
} catch (SdkException abortFailure) {
cause.addSuppressed(abortFailure);
}
}

private static RequestBody requestBodyFor(byte[] buffer, int length) {
return RequestBody.fromInputStream(new ByteArrayInputStream(buffer, 0, length), length);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The async S3 client can handle multipart uploads natively, without us having to write logic to handle it.

It also supports parallel uploads, which make the upload process faster.

Why not use that client instead? https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/best-practices-s3-uploads.html

…credentials, configurable path-style, region none, native multipart via S3AsyncClient
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

@AndreasVAmexio
AndreasVAmexio self-requested a review August 4, 2026 13:45
@AndreasVAmexio
AndreasVAmexio dismissed their stale review August 4, 2026 13:46

Code was updated

Comment on lines +29 to +32
private final S3Client client;

@NonNull
private final S3AsyncClient asyncClient;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why use 2 clients? Can't everything be be done with the S3AsyncClient without any problems?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand why this suddenly has to be moved to a separate autoconfiguration.

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.

Consequence of the package-private factory

private void writeObjectMultipart(ContentReference contentReference, InputStream inputStream, byte[] buffer)
throws IOException {
var uploadId = client.createMultipartUpload(CreateMultipartUploadRequest.builder()
var requestBody = AsyncRequestBody.forBlockingInputStream(null); // length unknown

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wny not use AsyncRequestBody.fromInputStream(inputStream, null); instead of managing a separate thread ourselves?

That makes use of an internal threadpool, which saves on spawning threads and on the complexity of this code.

.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.multipartEnabled(true)
.multipartConfiguration(MultipartConfiguration.builder()
.thresholdInBytes(PART_SIZE)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

According to the javadoc, this defaults to the same value as minimumPartSizeInBytes, so there is no reason to specify this.

implementation project(':contentgrid-appserver-blueprintartifact-impl-utils')
implementation 'io.minio:minio'
implementation('software.amazon.awssdk:s3') {
// Only the synchronous ApacheHttpClient is used; keep the Netty-based async client off the classpath

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@michyspiri Did you miss this question?

Comment thread README.md
| Property | Description | Default | Required |
|---|---|---|---|
| `contentgrid.appserver.content.s3.url` | S3 endpoint URL (e.g. `https://s3.amazonaws.com` or a MinIO URL). | — | Yes |
| `contentgrid.appserver.content.s3.url` | S3 endpoint URL, including the scheme (e.g. `https://s3.amazonaws.com` or a MinIO URL). | — | Yes |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well yes, a URL includes the scheme, otherwise it is not a URL.

Suggested change
| `contentgrid.appserver.content.s3.url` | S3 endpoint URL, including the scheme (e.g. `https://s3.amazonaws.com` or a MinIO URL). || Yes |
| `contentgrid.appserver.content.s3.url` | S3 endpoint URL (e.g. `https://s3.amazonaws.com` or a MinIO URL). || Yes |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants