This is an automated email from the ASF dual-hosted git repository.
etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 7480c3bc2b AWS: Schedule next credential refresh (#15732)
7480c3bc2b is described below
commit 7480c3bc2be4c24750fbf39320f845e578a4f03b
Author: Eduard Tudenhoefner <[email protected]>
AuthorDate: Mon Mar 23 16:32:28 2026 +0100
AWS: Schedule next credential refresh (#15732)
---
.../java/org/apache/iceberg/aws/s3/S3FileIO.java | 1 +
.../aws/s3/TestS3FileIOCredentialRefresh.java | 109 +++++++++++++++++++++
2 files changed, 110 insertions(+)
diff --git a/aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java
b/aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java
index f045fcfde6..50e507b0cd 100644
--- a/aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java
+++ b/aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java
@@ -470,6 +470,7 @@ public class S3FileIO
if (!refreshed.isEmpty() && !isResourceClosed.get()) {
this.storageCredentials = Lists.newArrayList(refreshed);
+ scheduleCredentialRefresh();
}
} catch (Exception e) {
LOG.warn("Failed to refresh storage credentials", e);
diff --git
a/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3FileIOCredentialRefresh.java
b/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3FileIOCredentialRefresh.java
index 686a6b1277..0a8b0e0848 100644
---
a/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3FileIOCredentialRefresh.java
+++
b/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3FileIOCredentialRefresh.java
@@ -70,6 +70,115 @@ public class TestS3FileIOCredentialRefresh {
mockServer.reset();
}
+ @Test
+ public void credentialRefreshSchedulesNextRefresh() {
+ String nearExpiryMs = Long.toString(Instant.now().plus(3,
ChronoUnit.MINUTES).toEpochMilli());
+
+ StorageCredential initialCredential =
+ StorageCredential.create(
+ "s3://bucket/path",
+ ImmutableMap.of(
+ S3FileIOProperties.ACCESS_KEY_ID,
+ "initialAccessKey",
+ S3FileIOProperties.SECRET_ACCESS_KEY,
+ "initialSecretKey",
+ S3FileIOProperties.SESSION_TOKEN,
+ "initialToken",
+ S3FileIOProperties.SESSION_TOKEN_EXPIRES_AT_MS,
+ nearExpiryMs));
+
+ String firstRefreshExpiryMs =
+ Long.toString(Instant.now().plus(2,
ChronoUnit.MINUTES).toEpochMilli());
+ String secondRefreshExpiryMs =
+ Long.toString(Instant.now().plus(1, ChronoUnit.HOURS).toEpochMilli());
+
+ LoadCredentialsResponse firstRefreshResponse =
+ ImmutableLoadCredentialsResponse.builder()
+ .addCredentials(
+ ImmutableCredential.builder()
+ .prefix("s3://bucket/path")
+ .config(
+ ImmutableMap.of(
+ S3FileIOProperties.ACCESS_KEY_ID,
+ "firstRefreshedAccessKey",
+ S3FileIOProperties.SECRET_ACCESS_KEY,
+ "firstRefreshedSecretKey",
+ S3FileIOProperties.SESSION_TOKEN,
+ "firstRefreshedToken",
+ S3FileIOProperties.SESSION_TOKEN_EXPIRES_AT_MS,
+ firstRefreshExpiryMs))
+ .build())
+ .build();
+
+ LoadCredentialsResponse secondRefreshResponse =
+ ImmutableLoadCredentialsResponse.builder()
+ .addCredentials(
+ ImmutableCredential.builder()
+ .prefix("s3://bucket/path")
+ .config(
+ ImmutableMap.of(
+ S3FileIOProperties.ACCESS_KEY_ID,
+ "secondRefreshedAccessKey",
+ S3FileIOProperties.SECRET_ACCESS_KEY,
+ "secondRefreshedSecretKey",
+ S3FileIOProperties.SESSION_TOKEN,
+ "secondRefreshedToken",
+ S3FileIOProperties.SESSION_TOKEN_EXPIRES_AT_MS,
+ secondRefreshExpiryMs))
+ .build())
+ .build();
+
+ HttpRequest mockRequest =
request("/v1/credentials").withMethod(HttpMethod.GET.name());
+ mockServer
+ .when(mockRequest, org.mockserver.matchers.Times.once())
+ .respond(
+
response(LoadCredentialsResponseParser.toJson(firstRefreshResponse))
+ .withStatusCode(200));
+ mockServer
+ .when(mockRequest, org.mockserver.matchers.Times.unlimited())
+ .respond(
+
response(LoadCredentialsResponseParser.toJson(secondRefreshResponse))
+ .withStatusCode(200));
+
+ Map<String, String> properties =
+ ImmutableMap.of(
+ AwsProperties.CLIENT_FACTORY,
+ StaticClientFactory.class.getName(),
+ VendedCredentialsProvider.URI,
+ CREDENTIALS_URI,
+ CatalogProperties.URI,
+ CATALOG_URI,
+ "init-creation-stacktrace",
+ "false");
+
+ StaticClientFactory.client = null;
+ try (S3FileIO fileIO = new S3FileIO()) {
+ fileIO.initialize(properties);
+ fileIO.setCredentials(List.of(initialCredential));
+
+ fileIO.client();
+
+ // the first refresh returns near-expiry credentials, which should
schedule a second refresh
+ Awaitility.await()
+ .atMost(30, TimeUnit.SECONDS)
+ .untilAsserted(() -> mockServer.verify(mockRequest,
VerificationTimes.atLeast(2)));
+
+ Awaitility.await()
+ .atMost(10, TimeUnit.SECONDS)
+ .untilAsserted(
+ () -> {
+ List<StorageCredential> credentials = fileIO.credentials();
+ assertThat(credentials).hasSize(1);
+ assertThat(credentials.get(0).config())
+ .containsEntry(S3FileIOProperties.ACCESS_KEY_ID,
"secondRefreshedAccessKey")
+ .containsEntry(S3FileIOProperties.SECRET_ACCESS_KEY,
"secondRefreshedSecretKey")
+ .containsEntry(S3FileIOProperties.SESSION_TOKEN,
"secondRefreshedToken")
+ .containsEntry(
+ S3FileIOProperties.SESSION_TOKEN_EXPIRES_AT_MS,
secondRefreshExpiryMs);
+ });
+ }
+ }
+
@Test
public void credentialRefreshWithinFiveMinuteWindow() {
// Set up credentials expiring within the next 5 minutes so the refresh
triggers immediately