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

Reply via email to