Skip to content

Commit 5d49183

Browse files
committed
Merge branch 'master' into localstack-cleanup
2 parents 7f7af4c + 310adec commit 5d49183

8 files changed

Lines changed: 432 additions & 28 deletions

File tree

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## v3.1.0 - In Progress
22

3+
### Stories
4+
* [MSEARCH-1245](https://issues.folio.org/browse/MSEARCH-1245) Provide http client configuration for MinioS3Client/AwsS3Client
5+
* [MSEARCH-1245](https://issues.folio.org/browse/MSEARCH-1245) Fix bucket creation
6+
37
## v3.0.0 - Released
48
This release contains improvement client configuration and new features
59

pom.xml

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,16 @@
2020
<maven.compiler.source>21</maven.compiler.source>
2121
<maven.compiler.target>21</maven.compiler.target>
2222
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
23-
<lombok.version>1.18.36</lombok.version>
24-
<apache.commons.lang3.version>3.17.0</apache.commons.lang3.version>
23+
<lombok.version>1.18.44</lombok.version>
24+
<apache.commons.lang3.version>3.20.0</apache.commons.lang3.version>
2525
<minio.version>8.6.0</minio.version>
26-
<okhttp.version>5.1.0</okhttp.version>
27-
<log4j.version>2.23.1</log4j.version>
28-
<aws.sdk.version>2.40.17</aws.sdk.version>
29-
<junit.version>5.12.0</junit.version>
30-
<testcontainers.version>1.20.5</testcontainers.version>
31-
<commons-io.version>2.18.0</commons-io.version>
32-
<sonar.exclusions>
33-
**/src/main/java/org/folio/s3/client/S3ClientProperties.java
34-
</sonar.exclusions>
26+
<okhttp.version>5.3.2</okhttp.version>
27+
<log4j.version>2.25.4</log4j.version>
28+
<aws.sdk.version>2.42.34</aws.sdk.version>
29+
<junit.version>6.0.3</junit.version>
30+
<testcontainers.version>2.0.4</testcontainers.version>
31+
<mockito.version>5.18.0</mockito.version>
32+
<commons-io.version>2.21.0</commons-io.version>
3533
</properties>
3634

3735
<dependencies>
@@ -72,6 +70,11 @@
7270
<artifactId>s3-transfer-manager</artifactId>
7371
<version>${aws.sdk.version}</version>
7472
</dependency>
73+
<dependency>
74+
<groupId>software.amazon.awssdk</groupId>
75+
<artifactId>netty-nio-client</artifactId>
76+
<version>${aws.sdk.version}</version>
77+
</dependency>
7578
<dependency>
7679
<groupId>org.junit.jupiter</groupId>
7780
<artifactId>junit-jupiter-engine</artifactId>
@@ -84,6 +87,12 @@
8487
<version>${junit.version}</version>
8588
<scope>test</scope>
8689
</dependency>
90+
<dependency>
91+
<groupId>org.mockito</groupId>
92+
<artifactId>mockito-junit-jupiter</artifactId>
93+
<version>${mockito.version}</version>
94+
<scope>test</scope>
95+
</dependency>
8796
<dependency>
8897
<groupId>org.testcontainers</groupId>
8998
<artifactId>testcontainers</artifactId>
@@ -92,7 +101,7 @@
92101
</dependency>
93102
<dependency>
94103
<groupId>org.testcontainers</groupId>
95-
<artifactId>localstack</artifactId>
104+
<artifactId>testcontainers-localstack</artifactId>
96105
<version>${testcontainers.version}</version>
97106
<scope>test</scope>
98107
</dependency>
@@ -104,7 +113,7 @@
104113
<plugin>
105114
<groupId>org.codehaus.mojo</groupId>
106115
<artifactId>versions-maven-plugin</artifactId>
107-
<version>2.16.1</version>
116+
<version>2.21.0</version>
108117
<executions>
109118
<execution>
110119
<id>display-dependency</id>
@@ -122,7 +131,7 @@
122131
<plugin>
123132
<groupId>org.apache.maven.plugins</groupId>
124133
<artifactId>maven-enforcer-plugin</artifactId>
125-
<version>3.4.1</version>
134+
<version>3.6.2</version>
126135
<executions>
127136
<execution>
128137
<id>enforce-maven</id>
@@ -143,7 +152,7 @@
143152
<plugin>
144153
<groupId>org.codehaus.mojo</groupId>
145154
<artifactId>build-helper-maven-plugin</artifactId>
146-
<version>3.4.0</version>
155+
<version>3.6.1</version>
147156
<executions>
148157
<execution>
149158
<phase>generate-sources</phase>
@@ -161,7 +170,7 @@
161170

162171
<plugin>
163172
<artifactId>maven-source-plugin</artifactId>
164-
<version>3.3.0</version>
173+
<version>3.4.0</version>
165174
<executions>
166175
<execution>
167176
<id>attach-sources</id>
@@ -175,7 +184,7 @@
175184

176185
<plugin>
177186
<artifactId>maven-javadoc-plugin</artifactId>
178-
<version>3.6.0</version>
187+
<version>3.12.0</version>
179188
<executions>
180189
<execution>
181190
<id>attach-javadocs</id>
@@ -189,7 +198,7 @@
189198

190199
<plugin>
191200
<artifactId>maven-deploy-plugin</artifactId>
192-
<version>3.1.1</version>
201+
<version>3.1.4</version>
193202
<executions>
194203
<execution>
195204
<id>deploy</id>
@@ -204,7 +213,7 @@
204213
<plugin>
205214
<groupId>org.apache.maven.plugins</groupId>
206215
<artifactId>maven-compiler-plugin</artifactId>
207-
<version>3.14.0</version>
216+
<version>3.15.0</version>
208217
<configuration>
209218
<compilerVersion>21</compilerVersion>
210219
</configuration>
@@ -213,7 +222,7 @@
213222
<plugin>
214223
<groupId>org.apache.maven.plugins</groupId>
215224
<artifactId>maven-release-plugin</artifactId>
216-
<version>3.0.1</version>
225+
<version>3.3.1</version>
217226
<configuration>
218227
<preparationGoals>clean verify</preparationGoals>
219228
<tagNameFormat>v@{project.version}</tagNameFormat>
@@ -225,7 +234,7 @@
225234
<plugin>
226235
<groupId>org.apache.maven.plugins</groupId>
227236
<artifactId>maven-surefire-plugin</artifactId>
228-
<version>3.2.1</version>
237+
<version>3.5.5</version>
229238
</plugin>
230239

231240
</plugins>

src/main/java/org/folio/s3/client/AwsS3Client.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.InputStream;
77
import java.io.SequenceInputStream;
88
import java.net.URI;
9+
import java.time.Duration;
910
import java.util.concurrent.Executors;
1011

1112
import org.folio.s3.exception.S3ClientException;
@@ -26,6 +27,7 @@
2627
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
2728
import software.amazon.awssdk.services.s3.model.UploadPartCopyRequest;
2829
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
30+
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
2931
import software.amazon.awssdk.transfer.s3.S3TransferManager;
3032
import software.amazon.awssdk.transfer.s3.model.UploadRequest;
3133

@@ -61,13 +63,29 @@ static S3AsyncClient createS3Client(S3ClientProperties s3ClientProperties) {
6163

6264
credentialsProvider = getCredentialsProvider(accessKey, secretKey);
6365

64-
return S3AsyncClient.builder()
65-
.endpointOverride(URI.create(endpoint))
66-
.forcePathStyle(s3ClientProperties.isForcePathStyle())
67-
.region(Region.of(region))
68-
.credentialsProvider(credentialsProvider)
69-
.multipartEnabled(true)
70-
.build();
66+
var s3Builder = S3AsyncClient.builder()
67+
.endpointOverride(URI.create(endpoint))
68+
.forcePathStyle(s3ClientProperties.isForcePathStyle())
69+
.region(Region.of(region))
70+
.credentialsProvider(credentialsProvider)
71+
.multipartEnabled(true);
72+
73+
var idleKeepAlive = s3ClientProperties.getIdleKeepAliveSeconds();
74+
var maxConcurrency = s3ClientProperties.getMaxRequestsPerHost();
75+
if (idleKeepAlive != null || maxConcurrency != null) {
76+
var httpBuilder = NettyNioAsyncHttpClient.builder();
77+
if (idleKeepAlive != null) {
78+
log.info("Configuring Netty HTTP client: connectionMaxIdleTime={}s", idleKeepAlive);
79+
httpBuilder.connectionMaxIdleTime(Duration.ofSeconds(idleKeepAlive));
80+
}
81+
if (maxConcurrency != null) {
82+
log.info("Configuring Netty HTTP client: maxConcurrency={}", maxConcurrency);
83+
httpBuilder.maxConcurrency(maxConcurrency);
84+
}
85+
s3Builder.httpClient(httpBuilder.build());
86+
}
87+
88+
return s3Builder.build();
7189
}
7290

7391
@Override

src/main/java/org/folio/s3/client/MinioS3Client.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
import io.minio.messages.DeleteObject;
4848
import io.minio.messages.Part;
4949
import lombok.extern.log4j.Log4j2;
50+
import okhttp3.ConnectionPool;
51+
import okhttp3.Dispatcher;
52+
import okhttp3.OkHttpClient;
5053

5154
@Log4j2
5255
// 2142: we wrap and rethrow InterruptedException as S3ClientException
@@ -90,6 +93,23 @@ static ExtendedMinioAsyncClient createClient(S3ClientProperties properties) {
9093
builder.region(region);
9194
}
9295

96+
var idleKeepAliveSeconds = properties.getIdleKeepAliveSeconds();
97+
if (idleKeepAliveSeconds != null) {
98+
int maxIdle = properties.getMaxIdleConnections() != null ? properties.getMaxIdleConnections() : 5;
99+
log.info("Configuring OkHttp connection pool: idleKeepAlive={}s maxIdleConnections={}", idleKeepAliveSeconds, maxIdle);
100+
var pool = new ConnectionPool(maxIdle, idleKeepAliveSeconds, TimeUnit.SECONDS);
101+
var dispatcher = new Dispatcher();
102+
if (properties.getMaxRequestsPerHost() != null) {
103+
log.info("Configuring OkHttp Dispatcher: maxRequestsPerHost={}", properties.getMaxRequestsPerHost());
104+
dispatcher.setMaxRequestsPerHost(properties.getMaxRequestsPerHost());
105+
}
106+
var httpClient = new OkHttpClient.Builder()
107+
.connectionPool(pool)
108+
.dispatcher(dispatcher)
109+
.build();
110+
builder.httpClient(httpClient);
111+
}
112+
93113
Provider provider;
94114
if (StringUtils.isNotBlank(accessKey) && StringUtils.isNotBlank(secretKey)) {
95115
provider = new StaticProvider(accessKey, secretKey, null);
@@ -115,6 +135,7 @@ public void createBucketIfNotExists() {
115135
.get();
116136
if (Boolean.TRUE.equals(exists)) {
117137
log.debug("Bucket already exists.");
138+
return;
118139
}
119140
client.makeBucket(MakeBucketArgs.builder()
120141
.bucket(bucket)

src/main/java/org/folio/s3/client/S3ClientProperties.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,36 @@ public class S3ClientProperties {
4646
* True for bucket name in the path, false for bucket name in the virtual host name.
4747
*/
4848
private boolean forcePathStyle;
49+
50+
/**
51+
* Maximum time, in seconds, that an idle HTTP connection in the OkHttp connection pool
52+
* (used by the underlying Minio client) is kept alive before being evicted.
53+
*
54+
* <p>If {@code null}, the OkHttp default of 5 minutes is used. Set this to a value smaller
55+
* than the server-side idle timeout (AWS S3 closes idle connections after ~20s) to avoid
56+
* "unexpected end of stream" / connection-reset errors, especially during multipart uploads
57+
* — which always go through the Minio client, even when {@link AwsS3Client} is used.
58+
*/
59+
private Integer idleKeepAliveSeconds;
60+
61+
/**
62+
* Maximum number of idle connections the OkHttp connection pool retains.
63+
*
64+
* <p>If {@code null}, defaults to 5 (OkHttp built-in default). Only takes effect when
65+
* {@link #idleKeepAliveSeconds} is also set. Increase this when a single shared
66+
* {@link FolioS3Client} instance services many concurrent callers so that connections
67+
* from finished requests are available for immediate reuse.
68+
*/
69+
private Integer maxIdleConnections;
70+
71+
/**
72+
* Maximum number of concurrent HTTP requests OkHttp will dispatch to the same host
73+
* (maps to {@code OkHttp Dispatcher.maxRequestsPerHost}).
74+
*
75+
* <p>If {@code null}, defaults to 5 (OkHttp built-in default). Only takes effect when
76+
* {@link #idleKeepAliveSeconds} is also set. When a shared client handles more than 5
77+
* concurrent callers, OkHttp queues the excess requests, which blocks the calling threads.
78+
* Set this to at least the expected concurrency to avoid thread starvation.
79+
*/
80+
private Integer maxRequestsPerHost;
4981
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.folio.s3.client;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
/**
9+
* Verifies that {@link AwsS3Client#createS3Client} correctly handles the
10+
* connection-pool properties from {@link S3ClientProperties}.
11+
*
12+
* <p>The AWS SDK's {@link software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient}
13+
* does not expose its configuration after construction, so these tests focus on
14+
* verifying that the client is created successfully for each combination rather
15+
* than inspecting the internal Netty settings via reflection.
16+
*/
17+
class AwsS3ClientHttpClientTest {
18+
19+
private static final String ENDPOINT = "http://localhost:9000";
20+
21+
@Test
22+
@DisplayName("S3AsyncClient is created when no pool properties are set (default Netty config)")
23+
void defaultNettyConfigWhenNoPropertiesSet() {
24+
var props = baseProps().build();
25+
26+
var client = AwsS3Client.createS3Client(props);
27+
28+
assertNotNull(client, "S3AsyncClient must be created with default Netty configuration");
29+
client.close();
30+
}
31+
32+
@Test
33+
@DisplayName("S3AsyncClient is created when only idleKeepAliveSeconds is set")
34+
void nettyConfigWithIdleKeepAliveOnly() {
35+
var props = baseProps().idleKeepAliveSeconds(10).build();
36+
37+
var client = AwsS3Client.createS3Client(props);
38+
39+
assertNotNull(client);
40+
client.close();
41+
}
42+
43+
@Test
44+
@DisplayName("S3AsyncClient is created when only maxRequestsPerHost is set")
45+
void nettyConfigWithMaxConcurrencyOnly() {
46+
var props = baseProps().maxRequestsPerHost(50).build();
47+
48+
var client = AwsS3Client.createS3Client(props);
49+
50+
assertNotNull(client);
51+
client.close();
52+
}
53+
54+
@Test
55+
@DisplayName("S3AsyncClient is created when both idleKeepAliveSeconds and maxRequestsPerHost are set")
56+
void nettyConfigWithBothPropertiesSet() {
57+
var props = baseProps().idleKeepAliveSeconds(10).maxRequestsPerHost(50).build();
58+
59+
var client = AwsS3Client.createS3Client(props);
60+
61+
assertNotNull(client);
62+
client.close();
63+
}
64+
65+
private static S3ClientProperties.S3ClientPropertiesBuilder baseProps() {
66+
return S3ClientProperties.builder()
67+
.endpoint(ENDPOINT)
68+
.region("us-east-1")
69+
.bucket("test-bucket")
70+
.accessKey("ak")
71+
.secretKey("sk")
72+
.forcePathStyle(true);
73+
}
74+
}

0 commit comments

Comments
 (0)