This is an automated email from the ASF dual-hosted git repository.

fanng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new d49e7eb463 [#4398] feat(core): support credential cache for Gravitino 
server (#5995)
d49e7eb463 is described below

commit d49e7eb463a666055d8dc55287b9b25c755d0f1a
Author: FANNG <[email protected]>
AuthorDate: Fri Dec 27 09:49:23 2024 +0800

    [#4398] feat(core): support credential cache for Gravitino server (#5995)
    
    ### What changes were proposed in this pull request?
    
    add credential cache for Gravitino server, not support for Iceberg rest
    server yet.
    
    ### Why are the changes needed?
    
    Fix: #4398
    
    ### Does this PR introduce _any_ user-facing change?
    no
    
    ### How was this patch tested?
    
    testing in local env, get credential from Gravitino server and see
    whether it's fetched from remote or local cache
---
 .../gravitino/credential/CredentialConstants.java  |   2 +
 .../org/apache/gravitino/config/ConfigBuilder.java |  25 +++++
 .../apache/gravitino/connector/PropertyEntry.java  |  24 +++++
 .../credential/CatalogCredentialContext.java       |  24 +++++
 .../credential/CatalogCredentialManager.java       |  21 ++++-
 .../gravitino/credential/CredentialCache.java      | 101 +++++++++++++++++++++
 .../gravitino/credential/CredentialCacheKey.java   |  64 +++++++++++++
 .../credential/PathBasedCredentialContext.java     |  33 +++++++
 .../credential/config/CredentialConfig.java        |  56 +++++++++++-
 .../credential/TestCredentialCacheKey.java         |  56 ++++++++++++
 10 files changed, 399 insertions(+), 7 deletions(-)

diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
index d2753f24b5..c766a86c14 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
@@ -22,6 +22,8 @@ package org.apache.gravitino.credential;
 public class CredentialConstants {
   public static final String CREDENTIAL_PROVIDER_TYPE = 
"credential-provider-type";
   public static final String CREDENTIAL_PROVIDERS = "credential-providers";
+  public static final String CREDENTIAL_CACHE_EXPIRE_RATIO = 
"credential-cache-expire-ratio";
+  public static final String CREDENTIAL_CACHE_MAX_SIZE = 
"credential-cache-max-size";
   public static final String S3_TOKEN_CREDENTIAL_PROVIDER = "s3-token";
   public static final String S3_TOKEN_EXPIRE_IN_SECS = 
"s3-token-expire-in-secs";
 
diff --git a/core/src/main/java/org/apache/gravitino/config/ConfigBuilder.java 
b/core/src/main/java/org/apache/gravitino/config/ConfigBuilder.java
index 148cda4fa7..d42f445e10 100644
--- a/core/src/main/java/org/apache/gravitino/config/ConfigBuilder.java
+++ b/core/src/main/java/org/apache/gravitino/config/ConfigBuilder.java
@@ -170,6 +170,31 @@ public class ConfigBuilder {
     return conf;
   }
 
+  /**
+   * Creates a configuration entry for Double data type.
+   *
+   * @return The created ConfigEntry instance for Double data type.
+   */
+  public ConfigEntry<Double> doubleConf() {
+    ConfigEntry<Double> conf =
+        new ConfigEntry<>(key, version, doc, alternatives, isPublic, 
isDeprecated);
+    Function<String, Double> func =
+        s -> {
+          if (s == null || s.isEmpty()) {
+            return null;
+          } else {
+            return Double.parseDouble(s);
+          }
+        };
+    conf.setValueConverter(func);
+
+    Function<Double, String> stringFunc =
+        t -> Optional.ofNullable(t).map(String::valueOf).orElse(null);
+    conf.setStringConverter(stringFunc);
+
+    return conf;
+  }
+
   /**
    * Creates a configuration entry for Boolean data type.
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java 
b/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
index b4c788a60d..a32c2fff21 100644
--- a/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
+++ b/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
 
 @Getter
 public final class PropertyEntry<T> {
+
   private final String name;
   private final String description;
   private final boolean required;
@@ -90,6 +91,7 @@ public final class PropertyEntry<T> {
   }
 
   public static class Builder<T> {
+
     private String name;
     private String description;
     private boolean required;
@@ -214,6 +216,28 @@ public final class PropertyEntry<T> {
         .build();
   }
 
+  public static PropertyEntry<Double> doublePropertyEntry(
+      String name,
+      String description,
+      boolean required,
+      boolean immutable,
+      double defaultValue,
+      boolean hidden,
+      boolean reserved) {
+    return new Builder<Double>()
+        .withName(name)
+        .withDescription(description)
+        .withRequired(required)
+        .withImmutable(immutable)
+        .withJavaType(Double.class)
+        .withDefaultValue(defaultValue)
+        .withDecoder(Double::parseDouble)
+        .withEncoder(String::valueOf)
+        .withHidden(hidden)
+        .withReserved(reserved)
+        .build();
+  }
+
   public static PropertyEntry<Integer> integerPropertyEntry(
       String name,
       String description,
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialContext.java
 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialContext.java
index a39dbba01b..6ac0c498c0 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialContext.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialContext.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.credential;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import javax.validation.constraints.NotNull;
 
@@ -35,4 +36,27 @@ public class CatalogCredentialContext implements 
CredentialContext {
   public String getUserName() {
     return userName;
   }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(userName);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || !(o instanceof CatalogCredentialContext)) {
+      return false;
+    }
+    return Objects.equal(userName, ((CatalogCredentialContext) o).userName);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder.append("User name: ").append(userName);
+    return stringBuilder.toString();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
index 2fe6fedccd..0e407a399b 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/CatalogCredentialManager.java
@@ -34,20 +34,21 @@ public class CatalogCredentialManager implements Closeable {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(CatalogCredentialManager.class);
 
+  private final CredentialCache<CredentialCacheKey> credentialCache;
+
   private final String catalogName;
   private final Map<String, CredentialProvider> credentialProviders;
 
   public CatalogCredentialManager(String catalogName, Map<String, String> 
catalogProperties) {
     this.catalogName = catalogName;
     this.credentialProviders = 
CredentialUtils.loadCredentialProviders(catalogProperties);
+    this.credentialCache = new CredentialCache();
+    credentialCache.initialize(catalogProperties);
   }
 
   public Credential getCredential(String credentialType, CredentialContext 
context) {
-    // todo: add credential cache
-    Preconditions.checkState(
-        credentialProviders.containsKey(credentialType),
-        String.format("Credential %s not found", credentialType));
-    return credentialProviders.get(credentialType).getCredential(context);
+    CredentialCacheKey credentialCacheKey = new 
CredentialCacheKey(credentialType, context);
+    return credentialCache.getCredential(credentialCacheKey, cacheKey -> 
doGetCredential(cacheKey));
   }
 
   @Override
@@ -67,4 +68,14 @@ public class CatalogCredentialManager implements Closeable {
               }
             });
   }
+
+  private Credential doGetCredential(CredentialCacheKey credentialCacheKey) {
+    String credentialType = credentialCacheKey.getCredentialType();
+    CredentialContext context = credentialCacheKey.getCredentialContext();
+    LOG.debug("Try get credential, credential type: {}, context: {}.", 
credentialType, context);
+    Preconditions.checkState(
+        credentialProviders.containsKey(credentialType),
+        String.format("Credential %s not found", credentialType));
+    return credentialProviders.get(credentialType).getCredential(context);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/CredentialCache.java 
b/core/src/main/java/org/apache/gravitino/credential/CredentialCache.java
new file mode 100644
index 0000000000..afbb09d50e
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/credential/CredentialCache.java
@@ -0,0 +1,101 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.gravitino.credential;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Expiry;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import org.apache.gravitino.credential.config.CredentialConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CredentialCache<T> implements Closeable {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(CredentialCache.class);
+
+  // Calculates the credential expire time in the cache.
+  static class CredentialExpireTimeCalculator<T> implements Expiry<T, 
Credential> {
+
+    private double credentialCacheExpireRatio;
+
+    public CredentialExpireTimeCalculator(double credentialCacheExpireRatio) {
+      this.credentialCacheExpireRatio = credentialCacheExpireRatio;
+    }
+
+    // Set expire time after add a credential in the cache.
+    @Override
+    public long expireAfterCreate(T key, Credential credential, long 
currentTime) {
+      long credentialExpireTime = credential.expireTimeInMs();
+      long timeToExpire = credentialExpireTime - System.currentTimeMillis();
+      if (timeToExpire <= 0) {
+        return 0;
+      }
+
+      timeToExpire = (long) (timeToExpire * credentialCacheExpireRatio);
+      return TimeUnit.MILLISECONDS.toNanos(timeToExpire);
+    }
+
+    // Not change expire time after update credential, this should not happen.
+    @Override
+    public long expireAfterUpdate(T key, Credential value, long currentTime, 
long currentDuration) {
+      return currentDuration;
+    }
+
+    // Not change expire time after read credential.
+    @Override
+    public long expireAfterRead(T key, Credential value, long currentTime, 
long currentDuration) {
+      return currentDuration;
+    }
+  }
+
+  private Cache<T, Credential> credentialCache;
+
+  public void initialize(Map<String, String> catalogProperties) {
+    CredentialConfig credentialConfig = new 
CredentialConfig(catalogProperties);
+    long cacheSize = 
credentialConfig.get(CredentialConfig.CREDENTIAL_CACHE_MAX_SIZE);
+    double cacheExpireRatio = 
credentialConfig.get(CredentialConfig.CREDENTIAL_CACHE_EXPIRE_RATIO);
+
+    this.credentialCache =
+        Caffeine.newBuilder()
+            .expireAfter(new CredentialExpireTimeCalculator(cacheExpireRatio))
+            .maximumSize(cacheSize)
+            .removalListener(
+                (cacheKey, credential, c) ->
+                    LOG.debug("Credential expire, cache key: {}.", cacheKey))
+            .build();
+  }
+
+  public Credential getCredential(T cacheKey, Function<T, Credential> 
credentialSupplier) {
+    return credentialCache.get(cacheKey, key -> 
credentialSupplier.apply(cacheKey));
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (credentialCache != null) {
+      credentialCache.invalidateAll();
+      credentialCache = null;
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/CredentialCacheKey.java 
b/core/src/main/java/org/apache/gravitino/credential/CredentialCacheKey.java
new file mode 100644
index 0000000000..1d0d8f7b3b
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/credential/CredentialCacheKey.java
@@ -0,0 +1,64 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.gravitino.credential;
+
+import java.util.Objects;
+import lombok.Getter;
+
+@Getter
+public class CredentialCacheKey {
+
+  private final String credentialType;
+  private final CredentialContext credentialContext;
+
+  public CredentialCacheKey(String credentialType, CredentialContext 
credentialContext) {
+    this.credentialType = credentialType;
+    this.credentialContext = credentialContext;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(credentialType, credentialContext);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || !(o instanceof CredentialCacheKey)) {
+      return false;
+    }
+    CredentialCacheKey that = (CredentialCacheKey) o;
+    return Objects.equals(credentialType, that.credentialType)
+        && Objects.equals(credentialContext, that.credentialContext);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder
+        .append("credentialType: ")
+        .append(credentialType)
+        .append("credentialContext: ")
+        .append(credentialContext);
+    return stringBuilder.toString();
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/PathBasedCredentialContext.java
 
b/core/src/main/java/org/apache/gravitino/credential/PathBasedCredentialContext.java
index 03e7bbe0e3..06d17b134b 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/PathBasedCredentialContext.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/PathBasedCredentialContext.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.credential;
 
 import com.google.common.base.Preconditions;
+import java.util.Objects;
 import java.util.Set;
 import javax.validation.constraints.NotNull;
 
@@ -55,4 +56,36 @@ public class PathBasedCredentialContext implements 
CredentialContext {
   public Set<String> getReadPaths() {
     return readPaths;
   }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(userName, writePaths, readPaths);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || !(o instanceof PathBasedCredentialContext)) {
+      return false;
+    }
+    PathBasedCredentialContext that = (PathBasedCredentialContext) o;
+    return Objects.equals(userName, that.userName)
+        && Objects.equals(writePaths, that.writePaths)
+        && Objects.equals(readPaths, that.readPaths);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder
+        .append("User name: ")
+        .append(userName)
+        .append(", write path: ")
+        .append(writePaths)
+        .append(", read path: ")
+        .append(readPaths);
+    return stringBuilder.toString();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/config/CredentialConfig.java
 
b/core/src/main/java/org/apache/gravitino/credential/config/CredentialConfig.java
index d8823417cd..31a5183cc2 100644
--- 
a/core/src/main/java/org/apache/gravitino/credential/config/CredentialConfig.java
+++ 
b/core/src/main/java/org/apache/gravitino/credential/config/CredentialConfig.java
@@ -21,16 +21,23 @@ package org.apache.gravitino.credential.config;
 
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
 import org.apache.gravitino.connector.PropertyEntry;
 import org.apache.gravitino.credential.CredentialConstants;
 
-public class CredentialConfig {
+public class CredentialConfig extends Config {
+
+  private static final long DEFAULT_CREDENTIAL_CACHE_MAX_SIZE = 10_000L;
+  private static final double DEFAULT_CREDENTIAL_CACHE_EXPIRE_RATIO = 0.15d;
 
   public static final Map<String, PropertyEntry<?>> 
CREDENTIAL_PROPERTY_ENTRIES =
       new ImmutableMap.Builder<String, PropertyEntry<?>>()
           .put(
               CredentialConstants.CREDENTIAL_PROVIDERS,
-              PropertyEntry.booleanPropertyEntry(
+              PropertyEntry.stringPropertyEntry(
                   CredentialConstants.CREDENTIAL_PROVIDERS,
                   "Credential providers for the Gravitino catalog, schema, 
fileset, table, etc.",
                   false /* required */,
@@ -38,5 +45,50 @@ public class CredentialConfig {
                   null /* default value */,
                   false /* hidden */,
                   false /* reserved */))
+          .put(
+              CredentialConstants.CREDENTIAL_CACHE_EXPIRE_RATIO,
+              PropertyEntry.doublePropertyEntry(
+                  CredentialConstants.CREDENTIAL_CACHE_EXPIRE_RATIO,
+                  "Ratio of the credential's expiration time when Gravitino 
remove credential from the cache.",
+                  false /* required */,
+                  false /* immutable */,
+                  DEFAULT_CREDENTIAL_CACHE_EXPIRE_RATIO /* default value */,
+                  false /* hidden */,
+                  false /* reserved */))
+          .put(
+              CredentialConstants.CREDENTIAL_CACHE_MAX_SIZE,
+              PropertyEntry.longPropertyEntry(
+                  CredentialConstants.CREDENTIAL_CACHE_MAX_SIZE,
+                  "Max size for the credential cache.",
+                  false /* required */,
+                  false /* immutable */,
+                  DEFAULT_CREDENTIAL_CACHE_MAX_SIZE /* default value */,
+                  false /* hidden */,
+                  false /* reserved */))
           .build();
+
+  public static final ConfigEntry<Double> CREDENTIAL_CACHE_EXPIRE_RATIO =
+      new ConfigBuilder(CredentialConstants.CREDENTIAL_CACHE_EXPIRE_RATIO)
+          .doc(
+              "Ratio of the credential's expiration time when Gravitino remove 
credential from the "
+                  + "cache.")
+          .version(ConfigConstants.VERSION_0_8_0)
+          .doubleConf()
+          .checkValue(
+              ratio -> ratio >= 0 && ratio < 1,
+              "Ratio of the credential's expiration time should greater than 
or equal to 0 "
+                  + "and less than 1.")
+          .createWithDefault(DEFAULT_CREDENTIAL_CACHE_EXPIRE_RATIO);
+
+  public static final ConfigEntry<Long> CREDENTIAL_CACHE_MAX_SIZE =
+      new ConfigBuilder(CredentialConstants.CREDENTIAL_CACHE_MAX_SIZE)
+          .doc("Max cache size for the credential.")
+          .version(ConfigConstants.VERSION_0_8_0)
+          .longConf()
+          .createWithDefault(DEFAULT_CREDENTIAL_CACHE_MAX_SIZE);
+
+  public CredentialConfig(Map<String, String> properties) {
+    super(false);
+    loadFromMap(properties, k -> true);
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/credential/TestCredentialCacheKey.java
 
b/core/src/test/java/org/apache/gravitino/credential/TestCredentialCacheKey.java
new file mode 100644
index 0000000000..b29c660a97
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/credential/TestCredentialCacheKey.java
@@ -0,0 +1,56 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.gravitino.credential;
+
+import java.util.Set;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableSet;
+
+public class TestCredentialCacheKey {
+
+  @Test
+  void testCredentialCacheKey() {
+
+    PathBasedCredentialContext context =
+        new PathBasedCredentialContext("user1", ImmutableSet.of("path1"), 
ImmutableSet.of("path2"));
+    PathBasedCredentialContext contextWithDiffUser =
+        new PathBasedCredentialContext("user2", ImmutableSet.of("path1"), 
ImmutableSet.of("path2"));
+    PathBasedCredentialContext contextWithDiffPath =
+        new PathBasedCredentialContext("user1", ImmutableSet.of("path3"), 
ImmutableSet.of("path4"));
+
+    CredentialCacheKey key1 = new CredentialCacheKey("s3-token", context);
+
+    Set<CredentialCacheKey> cache = ImmutableSet.of(key1);
+    Assertions.assertTrue(cache.contains(key1));
+
+    // different user
+    CredentialCacheKey key2 = new CredentialCacheKey("s3-token", 
contextWithDiffUser);
+    Assertions.assertFalse(cache.contains(key2));
+
+    // different path
+    CredentialCacheKey key3 = new CredentialCacheKey("s3-token", 
contextWithDiffPath);
+    Assertions.assertFalse(cache.contains(key3));
+
+    // different credential type
+    CredentialCacheKey key4 = new CredentialCacheKey("s3-token1", context);
+    Assertions.assertFalse(cache.contains(key4));
+  }
+}

Reply via email to