Skip to content

Commit 9a5c8e4

Browse files
committed
fix(retry): address bala review on PR #1701 review 4248622939
Five concessions from bala's review batch — each verified against minio-go's contract or the file's own pre-PR conventions: BaseS3Client.java - Restore the `protected static final Random RANDOM` field (with its java.security.SecureRandom + java.util.Random imports) that 60aeaed dropped when switching backoff jitter to ThreadLocalRandom. Per bala's r3205617158: do not remove protected/public API surface. The field is part of the public subclassing contract on BaseS3Client and is the Java analogue of minio-go's per-client `c.random` source (api.go:307); SDK-internal code paths still use ThreadLocalRandom.current() for the retry backoff hot path so jitter is unchanged. - Drop the null-guard around `regionCache.remove(s3request.bucket())` introduced in beead6e. The guard was only load-bearing for the contrived testNoRetryOn404 (listBuckets + 404 NoSuchBucket — a wire shape no spec-compliant S3 server emits since NoSuchBucket only applies to bucket-scoped ops); the test is rewritten in this commit to a realistic scenario, removing the dependency on the guard. Http.java - Drop the two-line `// Our RetryInterceptor handles transient failures with full-jitter backoff; defer entirely to it instead of layering OkHttp's connection-level retry on top.` comment from newDefaultClient() per bala's r3205589355: the retryOnConnectionFailure(false) call paired with addInterceptor(new RetryInterceptor()) is self-explanatory and the prose duplicated the RetryInterceptor Javadoc. - Drop the `Utils.validateNotNull(requestBody, "request body")` guard from the Body(okhttp3.RequestBody) constructor per bala's r3205613399; the three other Body constructors do not validate non-null and let the next-line dereference fail with the JVM's helpful-NPE message — restoring that local convention. - Drop the `private long fileOffset` field, its capture in the Body(RandomAccessFile,...) constructor, and the seek in toRequestBody() per bala's r3205614300. Empirical truth-table probe (4 trials toggling F = Body.fileOffset against C+R = createBody save/restore + RequestBody.position+seek) confirmed F is redundant when C+R are present: removing F alone keeps both first-attempt and retry-attempt body content correct (Trial 3), since the pre-Body-construction seek in createBody already places the file pointer where RequestBody.position will capture it on construction and writeTo() will seek back to on each network attempt. RetryTest.java - Rewrite testNoRetryOn404 to call removeBucket on a non-existent bucket. NoSuchBucket only applies to bucket-scoped operations on the S3 wire; pairing it with the bucket-less listBuckets() is a contrived scenario AWS S3 / MinIO never produce. The rewrite preserves coverage (404 does not retry, ErrorResponseException carries code "NoSuchBucket" and status 404, request count == 1) while exercising a wire shape the SDK actually has to handle, and uses a non-null bucket so the regionCache invalidation path runs with a real ConcurrentHashMap.remove(String) call. Validated locally on Java 17: - :api:check (compile + spotless + spotbugs + tests) PASS - :api:javadoc PASS
1 parent 7fe6e87 commit 9a5c8e4

3 files changed

Lines changed: 13 additions & 22 deletions

File tree

api/src/main/java/io/minio/BaseS3Client.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@
5151
import java.io.OutputStreamWriter;
5252
import java.io.PrintWriter;
5353
import java.nio.charset.StandardCharsets;
54+
import java.security.SecureRandom;
5455
import java.util.Arrays;
5556
import java.util.Collections;
5657
import java.util.HashMap;
5758
import java.util.Locale;
5859
import java.util.Map;
60+
import java.util.Random;
5961
import java.util.Set;
6062
import java.util.concurrent.CompletableFuture;
6163
import java.util.concurrent.CompletionException;
@@ -90,6 +92,7 @@ public abstract class BaseS3Client implements AutoCloseable {
9092
"ServerSideEncryptionConfigurationNotFoundError";
9193
// maximum allowed bucket policy size is 20KiB
9294
protected static final int MAX_BUCKET_POLICY_SIZE = 20 * 1024;
95+
protected static final Random RANDOM = new Random(new SecureRandom().nextLong());
9396
protected static final ObjectMapper OBJECT_MAPPER =
9497
JsonMapper.builder()
9598
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
@@ -512,10 +515,9 @@ private void onResponse(final Response response) throws IOException {
512515
response.header("x-amz-id-2"));
513516
}
514517

515-
// invalidate region cache if needed (bucket may be null for e.g. listBuckets)
516-
if (s3request.bucket() != null
517-
&& (errorResponse.code().equals(NO_SUCH_BUCKET)
518-
|| errorResponse.code().equals(RETRY_HEAD))) {
518+
// invalidate region cache if needed
519+
if (errorResponse.code().equals(NO_SUCH_BUCKET)
520+
|| errorResponse.code().equals(RETRY_HEAD)) {
519521
regionCache.remove(s3request.bucket());
520522
}
521523

api/src/main/java/io/minio/Http.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,6 @@ public static OkHttpClient newDefaultClient() {
623623
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
624624
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
625625
.protocols(Arrays.asList(Protocol.HTTP_1_1))
626-
// Our RetryInterceptor handles transient failures with full-jitter backoff; defer
627-
// entirely to it instead of layering OkHttp's connection-level retry on top.
628626
.retryOnConnectionFailure(false)
629627
.addInterceptor(new RetryInterceptor())
630628
.build();
@@ -846,7 +844,6 @@ private static String peekS3ErrorCode(okhttp3.Response response) {
846844
public static class Body {
847845
private okhttp3.RequestBody requestBody;
848846
private RandomAccessFile file;
849-
private long fileOffset;
850847
private ByteBuffer buffer;
851848
private byte[] data;
852849
private Long length;
@@ -864,7 +861,7 @@ public static class Body {
864861
* disabled for that call.
865862
*/
866863
public Body(okhttp3.RequestBody requestBody) {
867-
this.requestBody = Utils.validateNotNull(requestBody, "request body");
864+
this.requestBody = requestBody;
868865
this.contentType = requestBody.contentType();
869866
}
870867

@@ -877,11 +874,6 @@ public Body(
877874
String md5Hash) {
878875
if (length < 0) throw new IllegalArgumentException("valid length must be provided");
879876
this.file = file;
880-
try {
881-
this.fileOffset = file.getFilePointer();
882-
} catch (IOException e) {
883-
throw new IllegalStateException("failed to read file position", e);
884-
}
885877
set(length, contentType, sha256Hash, md5Hash);
886878
}
887879

@@ -955,14 +947,7 @@ public Headers headers() {
955947
/** Creates HTTP RequestBody for this body. */
956948
public RequestBody toRequestBody() throws MinioException {
957949
if (requestBody != null) return new RequestBody(requestBody);
958-
if (file != null) {
959-
try {
960-
file.seek(fileOffset);
961-
} catch (IOException e) {
962-
throw new MinioException(e);
963-
}
964-
return new RequestBody(file, length, contentType);
965-
}
950+
if (file != null) return new RequestBody(file, length, contentType);
966951
if (buffer != null) return new RequestBody(buffer, contentType);
967952
return new RequestBody(data, length.intValue(), contentType);
968953
}

api/src/test/java/io/minio/RetryTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,9 @@ public void testRetryOnRetryableS3CodeIn400Body() throws IOException, MinioExcep
282282

283283
@Test
284284
public void testNoRetryOn404() throws IOException, MinioException {
285+
// Bucket-scoped operation returning 404 NoSuchBucket — a wire-realistic shape
286+
// (NoSuchBucket only ever applies to bucket-scoped calls). Verifies that 404
287+
// does not trigger retry and that the parsed error surfaces both code and status.
285288
try (MockWebServer server = new MockWebServer()) {
286289
server.enqueue(xmlError(404, "NoSuchBucket"));
287290
server.start();
@@ -290,7 +293,8 @@ public void testNoRetryOn404() throws IOException, MinioException {
290293
MinioClient.builder().endpoint(server.url("").toString()).maxRetries(3).build();
291294
try {
292295
try {
293-
client.listBuckets();
296+
client.removeBucket(
297+
RemoveBucketArgs.builder().bucket("missing-bucket-for-no-retry").build());
294298
Assert.fail("expected ErrorResponseException");
295299
} catch (ErrorResponseException e) {
296300
Assert.assertEquals("NoSuchBucket", e.errorResponse().code());

0 commit comments

Comments
 (0)