Skip to content

Commit f552fd6

Browse files
authored
refactor(S3ClientProvider): Reduce complexity of generateClient (#248)
* refactor(S3ClientProvider): Extract getClientForBucket method, call from generateSyncClient and generateAsyncClient * refactor(S3ClientProvider): Extract getBucketRegionFromResponse * refactor(S3ClientProvider): Move getClientForRegion call outside of the try * refactor(S3ClientProvider): extract determineBucketLocation method * logs(S3ClientProvider): update * refactor(S3ClientProvider): extract methods to check status codes of S3Exceptions * refactor(S3ClientProvider): extract getBucketLocation and getBucketLocationFromHead methods * logger(S3ClientProvider): make it static private and final; use appropriate name * cleanup(S3ClientProvider): Remove constructor used only in tests * cleanup(S3ClientProvider): Reduce visibility of methods
1 parent 9766afe commit f552fd6

File tree

4 files changed

+78
-102
lines changed

4 files changed

+78
-102
lines changed

src/main/java/software/amazon/nio/spi/s3/S3ClientProvider.java

Lines changed: 69 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.time.Duration;
1111
import java.util.NoSuchElementException;
1212
import java.util.Set;
13+
import java.util.function.Function;
14+
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
1517
import software.amazon.awssdk.auth.credentials.AwsCredentials;
@@ -25,6 +27,7 @@
2527
import software.amazon.awssdk.core.retry.conditions.RetryOnStatusCodeCondition;
2628
import software.amazon.awssdk.core.retry.conditions.RetryOnThrottlingCondition;
2729
import software.amazon.awssdk.http.HttpStatusCode;
30+
import software.amazon.awssdk.http.SdkHttpResponse;
2831
import software.amazon.awssdk.regions.Region;
2932
import software.amazon.awssdk.services.s3.S3AsyncClient;
3033
import software.amazon.awssdk.services.s3.S3Client;
@@ -96,16 +99,12 @@ public class S3ClientProvider {
9699
);
97100
}
98101

99-
Logger logger = LoggerFactory.getLogger("S3ClientStoreProvider");
102+
static private final Logger logger = LoggerFactory.getLogger(S3ClientProvider.class);
100103

101104
public S3ClientProvider(S3NioSpiConfiguration c) {
102105
this.configuration = (c == null) ? new S3NioSpiConfiguration() : c;
103106
}
104107

105-
public S3ClientProvider() {
106-
this(null);
107-
}
108-
109108
public S3CrtAsyncClientBuilder asyncClientBuilder() {
110109
return asyncClientBuilder;
111110
}
@@ -122,7 +121,7 @@ public void asyncClientBuilder(final S3CrtAsyncClientBuilder builder) {
122121
*
123122
* @return a S3Client not bound to a region
124123
*/
125-
public S3Client universalClient() {
124+
S3Client universalClient() {
126125
return universalClient(false);
127126
}
128127

@@ -134,7 +133,7 @@ public S3Client universalClient() {
134133
* @param <T> type of AwsClient
135134
* @return a S3Client not bound to a region
136135
*/
137-
public <T extends AwsClient> T universalClient(boolean async) {
136+
<T extends AwsClient> T universalClient(boolean async) {
138137
return (T)((async) ? DEFAULT_ASYNC_CLIENT : DEFAULT_CLIENT);
139138
}
140139

@@ -161,54 +160,8 @@ protected S3AsyncClient generateAsyncClient(String bucket) {
161160
*
162161
* @return an S3 client appropriate for the region of the named bucket
163162
*/
164-
protected S3Client generateClient (String bucketName, S3Client locationClient) {
165-
logger.debug("generating client for bucket: '{}'", bucketName);
166-
S3Client bucketSpecificClient = null;
167-
168-
if ((configuration.getEndpoint() == null) || configuration.getEndpoint().isBlank()) {
169-
//
170-
// we try to locate a bucket only if no endpoint is provided, which
171-
// means we are dealing with AWS S3 buckets
172-
//
173-
String bucketLocation = null;
174-
try {
175-
logger.debug("determining bucket location with getBucketLocation");
176-
bucketLocation = locationClient.getBucketLocation(builder -> builder.bucket(bucketName)).locationConstraintAsString();
177-
bucketSpecificClient = this.clientForRegion(bucketLocation);
178-
} catch (S3Exception e) {
179-
if(e.statusCode() == 403) {
180-
logger.debug("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation", bucketName);
181-
try {
182-
final HeadBucketResponse headBucketResponse = locationClient.headBucket(builder -> builder.bucket(bucketName));
183-
bucketSpecificClient = this.clientForRegion(headBucketResponse.
184-
sdkHttpResponse()
185-
.firstMatchingHeader("x-amz-bucket-region")
186-
.orElseThrow(() -> new NoSuchElementException("Head Bucket Response doesn't include the header 'x-amz-bucket-region'")));
187-
} catch (S3Exception e2) {
188-
if (e2.statusCode() == 301) {
189-
bucketSpecificClient = this.clientForRegion(e2.awsErrorDetails().
190-
sdkHttpResponse()
191-
.firstMatchingHeader("x-amz-bucket-region")
192-
.orElseThrow(() -> new NoSuchElementException("Head Bucket Response doesn't include the header 'x-amz-bucket-region'")));
193-
} else {
194-
throw e2;
195-
}
196-
}
197-
} else {
198-
throw e;
199-
}
200-
}
201-
202-
//
203-
// if here, no S3 nor other client has been created yet and we do not
204-
// have a location; we'll let it figure out from the profile region
205-
//
206-
logger.warn("Unable to determine the region of bucket: '{}'. Generating a client for the profile region.", bucketName);
207-
}
208-
209-
return (bucketSpecificClient != null)
210-
? bucketSpecificClient
211-
: clientForRegion(configuration.getRegion());
163+
S3Client generateSyncClient(String bucketName, S3Client locationClient) {
164+
return getClientForBucket(bucketName, locationClient, this::clientForRegion);
212165
}
213166

214167
/**
@@ -221,55 +174,77 @@ protected S3Client generateClient (String bucketName, S3Client locationClient) {
221174
*
222175
* @return an S3 client appropriate for the region of the named bucket
223176
*/
224-
protected S3AsyncClient generateAsyncClient (String bucketName, S3Client locationClient) {
225-
logger.debug("generating asynchronous client for bucket: '{}'", bucketName);
226-
S3AsyncClient bucketSpecificClient = null;
177+
S3AsyncClient generateAsyncClient (String bucketName, S3Client locationClient) {
178+
return getClientForBucket(bucketName, locationClient, this::asyncClientForRegion);
179+
}
180+
181+
private <T extends AwsClient> T getClientForBucket(
182+
String bucketName,
183+
S3Client locationClient,
184+
Function<String, T> getClientForRegion
185+
) {
186+
logger.debug("generating client for bucket: '{}'", bucketName);
187+
T bucketSpecificClient = null;
227188

228189
if ((configuration.getEndpoint() == null) || configuration.getEndpoint().isBlank()) {
229190
//
230191
// we try to locate a bucket only if no endpoint is provided, which
231192
// means we are dealing with AWS S3 buckets
232193
//
233-
String bucketLocation = null;
234-
try {
235-
logger.debug("determining bucket location with getBucketLocation");
236-
bucketLocation = locationClient.getBucketLocation(builder -> builder.bucket(bucketName)).locationConstraintAsString();
237-
bucketSpecificClient = this.asyncClientForRegion(bucketLocation);
238-
} catch (S3Exception e) {
239-
if(e.statusCode() == 403) {
240-
logger.debug("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation", bucketName);
241-
try {
242-
final HeadBucketResponse headBucketResponse = locationClient.headBucket(builder -> builder.bucket(bucketName));
243-
bucketSpecificClient = this.asyncClientForRegion(headBucketResponse.sdkHttpResponse()
244-
.firstMatchingHeader("x-amz-bucket-region")
245-
.orElseThrow(() -> new NoSuchElementException("Head Bucket Response doesn't include the header 'x-amz-bucket-region'")));
246-
} catch (S3Exception e2) {
247-
if (e2.statusCode() == 301) {
248-
bucketSpecificClient = this.asyncClientForRegion(e2
249-
.awsErrorDetails()
250-
.sdkHttpResponse()
251-
.firstMatchingHeader("x-amz-bucket-region")
252-
.orElseThrow(() -> new NoSuchElementException("Head Bucket Response doesn't include the header 'x-amz-bucket-region'"))
253-
);
254-
} else {
255-
throw e2;
256-
}
257-
}
258-
} else {
259-
throw e;
260-
}
194+
String bucketLocation = determineBucketLocation(bucketName, locationClient);
195+
196+
if ( bucketLocation != null) {
197+
bucketSpecificClient = getClientForRegion.apply(bucketLocation);
198+
} else {
199+
// if here, no S3 nor other client has been created yet, and we do not
200+
// have a location; we'll let it figure out from the profile region
201+
logger.warn("Unable to determine the region of bucket: '{}'. Generating a client for the profile region.", bucketName);
261202
}
262-
263-
//
264-
// if here, no S3 nor other client has been created yet and we do not
265-
// have a location; we'll let it figure out from the profile region
266-
//
267-
logger.warn("Unable to determine the region of bucket: '{}'. Generating a client for the profile region.", bucketName);
268203
}
269204

270205
return (bucketSpecificClient != null)
271-
? bucketSpecificClient
272-
: asyncClientForRegion(configuration.getRegion());
206+
? bucketSpecificClient
207+
: getClientForRegion.apply(configuration.getRegion());
208+
}
209+
210+
private String determineBucketLocation(String bucketName, S3Client locationClient) {
211+
try {
212+
return getBucketLocation(bucketName, locationClient);
213+
} catch (S3Exception e) {
214+
if(isForbidden(e)) {
215+
return getBucketLocationFromHead(bucketName, locationClient);
216+
} else {
217+
throw e;
218+
}
219+
}
220+
}
221+
222+
private String getBucketLocation(String bucketName, S3Client locationClient) {
223+
logger.debug("determining bucket location with getBucketLocation");
224+
return locationClient.getBucketLocation(builder -> builder.bucket(bucketName)).locationConstraintAsString();
225+
}
226+
227+
private String getBucketLocationFromHead(String bucketName, S3Client locationClient) {
228+
try {
229+
logger.debug("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation", bucketName);
230+
final HeadBucketResponse headBucketResponse = locationClient.headBucket(builder -> builder.bucket(bucketName));
231+
return getBucketRegionFromResponse(headBucketResponse.sdkHttpResponse());
232+
} catch (S3Exception e) {
233+
if (isRedirect(e)) {
234+
return getBucketRegionFromResponse(e.awsErrorDetails().sdkHttpResponse());
235+
} else {
236+
throw e;
237+
}
238+
}
239+
}
240+
241+
private boolean isForbidden(S3Exception e) { return e.statusCode() == 403; }
242+
private boolean isRedirect(S3Exception e) { return e.statusCode() == 301; }
243+
244+
private String getBucketRegionFromResponse(SdkHttpResponse response) {
245+
return response.firstMatchingHeader("x-amz-bucket-region").orElseThrow(() ->
246+
new NoSuchElementException("Head Bucket Response doesn't include the header 'x-amz-bucket-region'")
247+
);
273248
}
274249

275250
private S3Client clientForRegion(String regionName) {

src/test/java/software/amazon/nio/spi/s3/FixedS3ClientProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ public class FixedS3ClientProvider extends S3ClientProvider {
1919
final public AwsClient client;
2020

2121
public FixedS3ClientProvider(S3AsyncClient client) {
22+
super(null);
2223
this.client = client;
2324
}
2425

2526
@Override
26-
public S3Client universalClient() {
27+
S3Client universalClient() {
2728
return (S3Client)client;
2829
}
2930

src/test/java/software/amazon/nio/spi/s3/S3ClientProviderTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ public class S3ClientProviderTest {
3737
S3ClientProvider provider;
3838

3939
@BeforeEach
40-
public void before() throws Exception {
41-
provider = new S3ClientProvider();
40+
public void before() {
41+
provider = new S3ClientProvider(null);
4242
}
4343

4444
@Test
4545
public void initialization() {
46-
final S3ClientProvider P = new S3ClientProvider();
46+
final S3ClientProvider P = new S3ClientProvider(null);
4747

4848
assertNotNull(P.configuration);
4949

@@ -80,7 +80,7 @@ public void testGenerateClientWith403Response() {
8080
.build());
8181

8282
// which should get you a client
83-
final S3Client s3Client = provider.generateClient("test-bucket", mockClient);
83+
final S3Client s3Client = provider.generateSyncClient("test-bucket", mockClient);
8484
assertNotNull(s3Client);
8585

8686
final InOrder inOrder = inOrder(mockClient);
@@ -159,7 +159,7 @@ public void testGenerateClientWith403Then301ResponsesNoHeader(){
159159
);
160160

161161
// then you should get a NoSuchElement exception when you try to get the header
162-
assertThrows(NoSuchElementException.class, () -> provider.generateClient("test-bucket", mockClient));
162+
assertThrows(NoSuchElementException.class, () -> provider.generateSyncClient("test-bucket", mockClient));
163163

164164
final InOrder inOrder = inOrder(mockClient);
165165
inOrder.verify(mockClient).getBucketLocation(anyConsumer());

src/test/java/software/amazon/nio/spi/s3/S3FileSystemTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ public void isReadOnly() {
8888

8989
@Test
9090
public void getAndSetClientProvider() {
91-
final S3ClientProvider P1 = new S3ClientProvider();
92-
final S3ClientProvider P2 = new S3ClientProvider();
91+
final S3ClientProvider P1 = new S3ClientProvider(null);
92+
final S3ClientProvider P2 = new S3ClientProvider(null);
9393
s3FileSystem.clientProvider(P1); then(s3FileSystem.clientProvider()).isSameAs(P1);
9494
s3FileSystem.clientProvider(P2); then(s3FileSystem.clientProvider()).isSameAs(P2);
9595
}

0 commit comments

Comments
 (0)