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

ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 23f3158c CASSSIDECAR-348: Add observability around authN and authZ 
caches (#262)
23f3158c is described below

commit 23f3158c867f350f930257c198c586d5e25a0883
Author: Saranya Krishnakumar <[email protected]>
AuthorDate: Tue Sep 30 13:35:41 2025 -0700

    CASSSIDECAR-348: Add observability around authN and authZ caches (#262)
    
    Patch by Saranya Krishnakumar; Reviewed by Francisco Guerrero, Yifan Cai 
for CASSSIDECAR-348
---
 CHANGES.txt                                        |   1 +
 conf/sidecar.yaml                                  |   5 +-
 .../apache/cassandra/sidecar/acl/AuthCache.java    |  47 +++-
 .../cassandra/sidecar/acl/IdentityToRoleCache.java |   7 +-
 .../acl/authorization/RoleAuthorizationsCache.java |   7 +-
 .../sidecar/acl/authorization/SuperUserCache.java  |   7 +-
 .../sidecar/config/CacheConfiguration.java         |   5 +
 .../config/yaml/CacheConfigurationImpl.java        |  21 +-
 .../sidecar/metrics/server/CacheMetrics.java       |   6 +
 .../sidecar/testing/IntegrationTestModule.java     |   1 +
 .../acl/CassandraIdentityExtractorTest.java        |  11 +-
 .../sidecar/acl/IdentityToRoleCacheTest.java       |  26 ++-
 .../sidecar/acl/RoleAuthorizationsCacheTest.java   | 258 ++++++++++++++++++---
 .../MutualTLSAuthenticationHandlerTest.java        |   3 +-
 .../MutualTlsAuthenticationHandlerFactoryTest.java |  13 +-
 .../acl/authorization/SuperUserCacheTest.java      |  15 +-
 .../sidecar/config/SidecarConfigurationTest.java   |   1 +
 .../config/yaml/CacheConfigurationImplTest.java    | 100 ++++++++
 18 files changed, 474 insertions(+), 60 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 07c4e007..9f1262a7 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.3.0
 -----
+ * Add observability around authN and authZ caches in Sidecar (CASSSIDECAR-348)
  * Thread race issue when adding excluded metric in FilteringMetricRegistry 
(CASSSIDECAR-349)
  * Add support for stateless JWT authentication using public keys 
(CASSSIDECAR-334)
  * Improve FilteringMetricRegistry implementation (CASSSIDECAR-347)
diff --git a/conf/sidecar.yaml b/conf/sidecar.yaml
index e6e6936a..809181ab 100644
--- a/conf/sidecar.yaml
+++ b/conf/sidecar.yaml
@@ -292,7 +292,10 @@ access_control:
 #    - spiffe://authorized/admin/identities
   permission_cache:
     enabled: true
-    expire_after_access: 5m
+    # refresh_after_write does async cache refreshes. expire_after_access 
removes the cache entry on expiry. Prefer
+    # setting refresh_after_write over expire_after_access for AuthCaches
+    # expire_after_access: 5m
+    refresh_after_write: 5m
     maximum_size: 1000
     warmup_retries: 5
     warmup_retry_interval: 2s
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java
index f60ab103..28e12857 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.sidecar.acl;
 
 import java.util.Collections;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -33,7 +34,9 @@ import io.vertx.core.eventbus.EventBus;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.concurrent.TaskExecutorPool;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
 import org.apache.cassandra.sidecar.exceptions.SchemaUnavailableException;
+import org.apache.cassandra.sidecar.metrics.CacheStatsCounter;
 import org.jetbrains.annotations.VisibleForTesting;
 
 import static 
org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SIDECAR_SCHEMA_INITIALIZED;
@@ -51,6 +54,7 @@ public abstract class AuthCache<K, V>
     private final Function<K, V> loadFunction;
     private final Supplier<Map<K, V>> bulkLoadFunction;
     private final CacheConfiguration config;
+    private final CacheStatsCounter cacheMetrics;
     private final TaskExecutorPool internalPool;
     // cache is null when AuthCache is disabled
     private volatile LoadingCache<K, V> cache;
@@ -61,7 +65,8 @@ public abstract class AuthCache<K, V>
                         ExecutorPools executorPools,
                         Function<K, V> loadFunction,
                         Supplier<Map<K, V>> bulkLoadFunction,
-                        CacheConfiguration cacheConfiguration)
+                        CacheConfiguration cacheConfiguration,
+                        CacheStatsCounter cacheMetrics)
     {
         this.name = name;
         this.vertx = vertx;
@@ -69,6 +74,7 @@ public abstract class AuthCache<K, V>
         this.loadFunction = loadFunction;
         this.bulkLoadFunction = bulkLoadFunction;
         this.config = cacheConfiguration;
+        this.cacheMetrics = Objects.requireNonNull(cacheMetrics, "cacheMetrics 
is required");
 
         if (this.config.enabled())
         {
@@ -119,15 +125,40 @@ public abstract class AuthCache<K, V>
         return Collections.unmodifiableMap(cache.asMap());
     }
 
+    /**
+     * Invalidate a key.
+     * @param k key to invalidate
+     */
+    public void invalidate(K k)
+    {
+        if (cache != null)
+        {
+            cache.invalidate(k);
+            logger.info("Cache entry with key={} has been invalidated", k);
+        }
+    }
+
     private LoadingCache<K, V> initCache()
     {
-        return Caffeine.newBuilder()
-                       // setting refreshAfterWrite and expireAfterWrite to 
same value makes sure no stale
-                       // data is fetched after expire time
-                       
.refreshAfterWrite(config.expireAfterAccess().quantity(), 
config.expireAfterAccess().unit())
-                       
.expireAfterWrite(config.expireAfterAccess().quantity(), 
config.expireAfterAccess().unit())
-                       .maximumSize(config.maximumSize())
-                       .build(loadFunction::apply);
+        if (config.refreshAfterWrite() == null && config.expireAfterAccess() 
== null)
+        {
+            throw new ConfigurationException(name +
+                                             " must be configured with either 
refreshAfterWrite or expireAfterAccess");
+        }
+
+        Caffeine<Object, Object> cacheBuilder
+        = Caffeine.newBuilder()
+                  .recordStats(() -> cacheMetrics)
+                  .maximumSize(config.maximumSize());
+        if (config.refreshAfterWrite() != null)
+        {
+            
cacheBuilder.refreshAfterWrite(config.refreshAfterWrite().quantity(), 
config.refreshAfterWrite().unit());
+        }
+        if (config.expireAfterAccess() != null)
+        {
+            
cacheBuilder.expireAfterAccess(config.expireAfterAccess().quantity(), 
config.expireAfterAccess().unit());
+        }
+        return cacheBuilder.build(loadFunction::apply);
     }
 
     private void configureSidecarServerEventListener()
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCache.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCache.java
index 1ede4188..752d2273 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCache.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCache.java
@@ -25,6 +25,7 @@ import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.exceptions.SchemaUnavailableException;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
 
 /**
  * Caches entries from system_auth.identity_to_role table. The table maps 
valid certificate identities to Cassandra
@@ -39,14 +40,16 @@ public class IdentityToRoleCache extends AuthCache<String, 
String>
     public IdentityToRoleCache(Vertx vertx,
                                ExecutorPools executorPools,
                                SidecarConfiguration sidecarConfiguration,
-                               SystemAuthDatabaseAccessor 
systemAuthDatabaseAccessor)
+                               SystemAuthDatabaseAccessor 
systemAuthDatabaseAccessor,
+                               SidecarMetrics sidecarMetrics)
     {
         super(NAME,
               vertx,
               executorPools,
               systemAuthDatabaseAccessor::findRoleFromIdentity,
               systemAuthDatabaseAccessor::findAllIdentityToRoles,
-              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration());
+              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration(),
+              sidecarMetrics.server().cache().identityToRoleCacheMetrics);
     }
 
     public boolean containsKey(String identity)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/RoleAuthorizationsCache.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/RoleAuthorizationsCache.java
index 7b04d2d3..d56a9023 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/RoleAuthorizationsCache.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/RoleAuthorizationsCache.java
@@ -32,6 +32,7 @@ import 
org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SidecarPermissionsDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.schema.SidecarSchema;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
 
 /**
  * Caches role and authorizations held by it. Entries from 
system_auth.role_permissions table in Cassandra and
@@ -51,7 +52,8 @@ public class RoleAuthorizationsCache extends 
AuthCache<String, Map<String, Set<A
                                    SidecarConfiguration sidecarConfiguration,
                                    SidecarSchema sidecarSchema,
                                    SystemAuthDatabaseAccessor 
systemAuthDatabaseAccessor,
-                                   SidecarPermissionsDatabaseAccessor 
sidecarPermissionsDatabaseAccessor)
+                                   SidecarPermissionsDatabaseAccessor 
sidecarPermissionsDatabaseAccessor,
+                                   SidecarMetrics sidecarMetrics)
     {
         super(NAME,
               vertx,
@@ -63,7 +65,8 @@ public class RoleAuthorizationsCache extends 
AuthCache<String, Map<String, Set<A
                                              
loadAuthorizations(systemAuthDatabaseAccessor,
                                                                 sidecarSchema,
                                                                 
sidecarPermissionsDatabaseAccessor)),
-              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration());
+              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration(),
+              sidecarMetrics.server().cache().rolePermissionsCacheMetrics);
     }
 
     /**
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCache.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCache.java
index a011a6c2..511af804 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCache.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCache.java
@@ -24,6 +24,7 @@ import org.apache.cassandra.sidecar.acl.AuthCache;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
 
 /**
  * Caches superuser status of cassandra roles. Returns true if the supplied 
role or any other role granted to it
@@ -38,14 +39,16 @@ public class SuperUserCache extends AuthCache<String, 
Boolean>
     public SuperUserCache(Vertx vertx,
                           ExecutorPools executorPools,
                           SidecarConfiguration sidecarConfiguration,
-                          SystemAuthDatabaseAccessor 
systemAuthDatabaseAccessor)
+                          SystemAuthDatabaseAccessor 
systemAuthDatabaseAccessor,
+                          SidecarMetrics sidecarMetrics)
     {
         super(NAME,
               vertx,
               executorPools,
               systemAuthDatabaseAccessor::isSuperUser,
               systemAuthDatabaseAccessor::findAllRolesToSuperuserStatus,
-              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration());
+              
sidecarConfiguration.accessControlConfiguration().permissionCacheConfiguration(),
+              sidecarMetrics.server().cache().superUserCacheMetrics);
     }
 
     public boolean isSuperUser(String role)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/config/CacheConfiguration.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/config/CacheConfiguration.java
index f1f6b4cd..61be7322 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/config/CacheConfiguration.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/config/CacheConfiguration.java
@@ -31,6 +31,11 @@ public interface CacheConfiguration
      */
     MillisecondBoundConfiguration expireAfterAccess();
 
+    /**
+     * @return the configured amount of time after which cache entries are 
refreshed.
+     */
+    MillisecondBoundConfiguration refreshAfterWrite();
+
     /**
      * @return the maximum number of entries the cache may contain
      */
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
index 2b5e0e9b..150864a4 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImpl.java
@@ -37,6 +37,8 @@ public class CacheConfigurationImpl implements 
CacheConfiguration
 
     protected MillisecondBoundConfiguration expireAfterAccess;
 
+    protected MillisecondBoundConfiguration refreshAfterWrite;
+
     @JsonProperty("maximum_size")
     protected final long maximumSize;
 
@@ -50,22 +52,24 @@ public class CacheConfigurationImpl implements 
CacheConfiguration
 
     public CacheConfigurationImpl()
     {
-        this(MillisecondBoundConfiguration.parse("1h"), 100, true, 5, 
MillisecondBoundConfiguration.parse("1s"));
+        this(null, null, 100, true, 5, 
MillisecondBoundConfiguration.parse("1s"));
     }
 
     @VisibleForTesting
     public CacheConfigurationImpl(MillisecondBoundConfiguration 
expireAfterAccess, long maximumSize)
     {
-        this(expireAfterAccess, maximumSize, true, 5, 
MillisecondBoundConfiguration.parse("1s"));
+        this(expireAfterAccess, MillisecondBoundConfiguration.parse("1h"), 
maximumSize, true, 5, MillisecondBoundConfiguration.parse("1s"));
     }
 
     public CacheConfigurationImpl(MillisecondBoundConfiguration 
expireAfterAccess,
+                                  MillisecondBoundConfiguration 
refreshAfterWrite,
                                   long maximumSize,
                                   boolean enabled,
                                   int warmupRetries,
                                   MillisecondBoundConfiguration 
warmupRetryInterval)
     {
         this.expireAfterAccess = expireAfterAccess;
+        this.refreshAfterWrite = refreshAfterWrite;
         this.maximumSize = maximumSize;
         this.enabled = enabled;
         this.warmupRetries = warmupRetries;
@@ -85,6 +89,19 @@ public class CacheConfigurationImpl implements 
CacheConfiguration
         this.expireAfterAccess = expireAfterAccess;
     }
 
+    @Override
+    @JsonProperty("refresh_after_write")
+    public MillisecondBoundConfiguration refreshAfterWrite()
+    {
+        return refreshAfterWrite;
+    }
+
+    @JsonProperty("refresh_after_write")
+    public void setRefreshAfterWrite(MillisecondBoundConfiguration 
refreshAfterWrite)
+    {
+        this.refreshAfterWrite = refreshAfterWrite;
+    }
+
     /**
      * Legacy property {@code expire_after_access_millis}
      *
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/CacheMetrics.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/CacheMetrics.java
index 4b612ade..3c478884 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/CacheMetrics.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/CacheMetrics.java
@@ -29,9 +29,15 @@ import static 
org.apache.cassandra.sidecar.handlers.snapshots.ListSnapshotHandle
 public class CacheMetrics
 {
     public final CacheStatsCounter snapshotCacheMetrics;
+    public final CacheStatsCounter identityToRoleCacheMetrics;
+    public final CacheStatsCounter superUserCacheMetrics;
+    public final CacheStatsCounter rolePermissionsCacheMetrics;
 
     public CacheMetrics(MetricRegistry globalMetricRegistry)
     {
         snapshotCacheMetrics = new CacheStatsCounter(globalMetricRegistry, 
SNAPSHOT_CACHE_NAME);
+        identityToRoleCacheMetrics = new 
CacheStatsCounter(globalMetricRegistry, "identity_to_role_cache");
+        superUserCacheMetrics = new CacheStatsCounter(globalMetricRegistry, 
"super_user_cache");
+        rolePermissionsCacheMetrics = new 
CacheStatsCounter(globalMetricRegistry, "role_permissions_cache");
     }
 }
diff --git 
a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
 
b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
index 16a0438e..8bc70252 100644
--- 
a/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
+++ 
b/server/src/test/integration/org/apache/cassandra/sidecar/testing/IntegrationTestModule.java
@@ -263,6 +263,7 @@ public class IntegrationTestModule extends AbstractModule
                                                   rbacConfig,
                                                   
Collections.singleton(ADMIN_IDENTITY),
                                                   new 
CacheConfigurationImpl(MillisecondBoundConfiguration.parse("1s"),
+                                                                             
MillisecondBoundConfiguration.parse("1s"),
                                                                              
100,
                                                                              
true,
                                                                              5,
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/CassandraIdentityExtractorTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/CassandraIdentityExtractorTest.java
index 8c0f78c5..ae7e8df4 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/CassandraIdentityExtractorTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/CassandraIdentityExtractorTest.java
@@ -36,6 +36,9 @@ import 
org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
 import org.apache.cassandra.testing.utils.tls.CertificateBuilder;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
@@ -49,14 +52,20 @@ import static org.mockito.Mockito.when;
  */
 class CassandraIdentityExtractorTest
 {
+    private static final MetricRegistryFactory FACTORY
+    = new MetricRegistryFactory(CassandraIdentityExtractorTest.class.getName(),
+                                Collections.emptyList(),
+                                Collections.emptyList());
     Vertx vertx;
     ExecutorPools executorPools;
+    SidecarMetrics sidecarMetrics;
 
     @BeforeEach
     void setup()
     {
         vertx = Vertx.vertx();
         executorPools = createdSharedTestPool(vertx);
+        sidecarMetrics = new SidecarMetricsImpl(FACTORY, null);
     }
 
     @AfterEach
@@ -137,7 +146,7 @@ class CassandraIdentityExtractorTest
         when(mockCacheConfig.maximumSize()).thenReturn(10L);
         
when(mockAccessControlConfig.permissionCacheConfiguration()).thenReturn(mockCacheConfig);
 
-        return new IdentityToRoleCache(vertx, executorPools, 
mockSidecarConfig, mockDbAccessor);
+        return new IdentityToRoleCache(vertx, executorPools, 
mockSidecarConfig, mockDbAccessor, sidecarMetrics);
     }
 
     private X509Certificate certificate(String identity) throws Exception
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCacheTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCacheTest.java
index dc5ddbbf..76200b84 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCacheTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/IdentityToRoleCacheTest.java
@@ -40,6 +40,9 @@ import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.schema.SystemAuthSchema;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
 import static 
org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SIDECAR_SCHEMA_INITIALIZED;
@@ -52,20 +55,27 @@ import static org.mockito.Mockito.when;
  */
 class IdentityToRoleCacheTest
 {
+    private static final MetricRegistryFactory FACTORY
+    = new MetricRegistryFactory(IdentityToRoleCacheTest.class.getName(),
+                                Collections.emptyList(),
+                                Collections.emptyList());
     Vertx vertx;
     ExecutorPools executorPools;
+    SidecarMetrics sidecarMetrics;
 
     @BeforeEach
     void setup()
     {
         vertx = Vertx.vertx();
         executorPools = createdSharedTestPool(vertx);
+        sidecarMetrics = new SidecarMetricsImpl(FACTORY, null);
     }
 
     @AfterEach
     void cleanup()
     {
         TestResourceReaper.create().with(vertx).with(executorPools).close();
+        FACTORY.getOrCreate().removeMatching((name, metric) -> true);
     }
 
     @Test
@@ -75,7 +85,7 @@ class IdentityToRoleCacheTest
         
when(mockDbAccessor.findRoleFromIdentity("spiffe://cassandra/sidecar/test")).thenReturn("cassandra-role");
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(Collections.singletonMap("spiffe://cassandra/sidecar/test",
 "cassandra-role"));
         SidecarConfiguration mockConfig = mockConfig();
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isTrue();
         
assertThat(identityToRoleCache.get("spiffe://cassandra/sidecar/test")).isEqualTo("cassandra-role");
         assertThat(identityToRoleCache.getAll().size()).isOne();
@@ -89,7 +99,7 @@ class IdentityToRoleCacheTest
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(Collections.singletonMap("spiffe://cassandra/sidecar/test",
 "cassandra-role"));
         SidecarConfiguration mockConfig = mockConfig();
         
when(mockConfig.accessControlConfiguration().permissionCacheConfiguration().enabled()).thenReturn(false);
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
         assertThat(identityToRoleCache.cache()).isNull();
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isFalse();
         // loaded with load function
@@ -110,12 +120,13 @@ class IdentityToRoleCacheTest
         } };
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(identityRoles);
         SidecarConfiguration mockConfig = mockConfig();
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isTrue();
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test2")).isTrue();
         
assertThat(identityToRoleCache.get("spiffe://cassandra/sidecar/test")).isEqualTo("cassandra-role");
         
assertThat(identityToRoleCache.get("spiffe://cassandra/sidecar/test2")).isEqualTo("cassandra-role2");
         assertThat(identityToRoleCache.getAll().size()).isEqualTo(2);
+        
assertThat(sidecarMetrics.server().cache().identityToRoleCacheMetrics.snapshot().hitCount()).isEqualTo(2);
     }
 
     @Test
@@ -125,7 +136,7 @@ class IdentityToRoleCacheTest
         
when(mockDbAccessor.findRoleFromIdentity("spiffe://cassandra/sidecar/test")).thenReturn("cassandra-role");
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(Collections.singletonMap("spiffe://cassandra/sidecar/test",
 "cassandra-role"));
         SidecarConfiguration mockConfig = mockConfig();
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
         assertThat(identityToRoleCache.cache().asMap().size()).isZero();
         // warming cache
         identityToRoleCache.warmUp(5);
@@ -143,7 +154,7 @@ class IdentityToRoleCacheTest
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(Collections.singletonMap("spiffe://cassandra/sidecar/test",
 "cassandra-role"));
 
         SidecarConfiguration mockConfig = mockConfig();
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
         assertThat(identityToRoleCache.cache().asMap().size()).isZero();
 
         // warming cache
@@ -155,6 +166,7 @@ class IdentityToRoleCacheTest
         assertThat(identityToRoleCache.cache().asMap().size()).isOne();
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isTrue();
         
assertThat(identityToRoleCache.get("spiffe://cassandra/sidecar/test")).isEqualTo("cassandra-role");
+        
assertThat(sidecarMetrics.server().cache().identityToRoleCacheMetrics.snapshot().hitCount()).isOne();
     }
 
     @Test
@@ -165,7 +177,7 @@ class IdentityToRoleCacheTest
         
when(mockDbAccessor.findAllIdentityToRoles()).thenReturn(Collections.emptyMap());
 
         SidecarConfiguration mockConfig = mockConfig();
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, mockDbAccessor, 
sidecarMetrics);
 
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isFalse();
         
assertThat(identityToRoleCache.get("spiffe://cassandra/sidecar/test")).isNull();
@@ -183,7 +195,7 @@ class IdentityToRoleCacheTest
 
         SidecarConfiguration mockConfig = mockConfig();
 
-        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, 
systemAuthDatabaseAccessor);
+        IdentityToRoleCache identityToRoleCache = new 
IdentityToRoleCache(vertx, executorPools, mockConfig, 
systemAuthDatabaseAccessor, sidecarMetrics);
         
assertThat(identityToRoleCache.containsKey("spiffe://cassandra/sidecar/test")).isFalse();
     }
 
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/RoleAuthorizationsCacheTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/RoleAuthorizationsCacheTest.java
index 1cef08b5..f0477a5a 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/RoleAuthorizationsCacheTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/RoleAuthorizationsCacheTest.java
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import com.github.benmanes.caffeine.cache.stats.CacheStats;
 import io.vertx.core.Vertx;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.auth.authorization.Authorization;
@@ -45,10 +46,15 @@ import 
org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SidecarPermissionsDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.db.schema.SidecarSchema;
+import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
 import static 
org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SIDECAR_SCHEMA_INITIALIZED;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -57,9 +63,14 @@ import static org.mockito.Mockito.when;
  */
 class RoleAuthorizationsCacheTest
 {
+    private static final MetricRegistryFactory FACTORY
+    = new MetricRegistryFactory(RoleAuthorizationsCacheTest.class.getName(),
+                                Collections.emptyList(),
+                                Collections.emptyList());
     Vertx vertx;
     SidecarSchema mockSidecarSchema;
     ExecutorPools executorPools;
+    SidecarMetrics sidecarMetrics;
 
     @BeforeEach
     void setup()
@@ -68,12 +79,14 @@ class RoleAuthorizationsCacheTest
         mockSidecarSchema = mock(SidecarSchema.class);
         when(mockSidecarSchema.isInitialized()).thenReturn(true);
         executorPools = createdSharedTestPool(vertx);
+        sidecarMetrics = new SidecarMetricsImpl(FACTORY, null);
     }
 
     @AfterEach
     void cleanup()
     {
         TestResourceReaper.create().with(vertx).with(executorPools).close();
+        FACTORY.getOrCreate().removeMatching((name, metric) -> true);
     }
 
     @Test
@@ -93,9 +106,13 @@ class RoleAuthorizationsCacheTest
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
         assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(2);
+        CacheStats initialCallStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(initialCallStats.hitCount()).isZero();
+        assertThat(initialCallStats.missCount()).isOne();
         assertThat(cache.getAll().size()).isOne();
 
         sidecarAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(BasicPermissions.STREAM_SNAPSHOT.toAuthorization())));
@@ -106,42 +123,88 @@ class RoleAuthorizationsCacheTest
 
         // New entries fetched during refreshes
         assertThat(cache.getAuthorizations("test_role2").size()).isOne();
+        CacheStats afterRefreshStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(afterRefreshStats.hitCount()).isZero();
+        assertThat(afterRefreshStats.missCount()).isOne();
         assertThat(cache.getAll().size()).isOne();
+        assertThat(afterRefreshStats.evictionCount()).isOne();
+
+        assertThat(cache.getAuthorizations("test_role2").size()).isOne();
+        CacheStats validEntryStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(validEntryStats.hitCount()).isOne();
+        assertThat(validEntryStats.missCount()).isZero();
+
+        // check for not existing role
+        cache.getAuthorizations("non_existing_role");
+        CacheStats afterMissStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(afterMissStats.missCount()).isEqualTo(0);
+        // It is a hit, since we load entire role_permissions table during 
each refresh
+        assertThat(afterMissStats.hitCount()).isOne();
     }
 
     @Test
-    void testNotFoundUser()
+    void testMultipleLoadCacheStats()
     {
         SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
-        Map<String, Set<Authorization>> cassandraAuthorizations = new 
HashMap<>();
-        cassandraAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(CassandraPermissions.SELECT.toAuthorization())));
-        
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
+        when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(new 
HashMap<>());
         Map<String, Set<Authorization>> sidecarAuthorizations = new 
HashMap<>();
         sidecarAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(BasicPermissions.CREATE_SNAPSHOT.toAuthorization())));
         SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
         
when(mockSidecarPermissionsAccessor.rolesToAuthorizations()).thenReturn(sidecarAuthorizations);
+        // high cache expire time to test load stats
+        SidecarConfiguration mockConfig = mockConfig(mockCacheConfig(false, 
true, "5m"));
+        RoleAuthorizationsCache cache = new RoleAuthorizationsCache(vertx,
+                                                                    
executorPools,
+                                                                    mockConfig,
+                                                                    
mockSidecarSchema,
+                                                                    
mockDbAccessor,
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
+
+        assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(1);
+        assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(1);
+        assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(1);
+        assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(1);
+        assertThat(cache.getAuthorizations("test_role1").size()).isEqualTo(1);
+
+        CacheStats multipleRetrievalStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(multipleRetrievalStats.hitCount()).isEqualTo(4);
+        assertThat(multipleRetrievalStats.loadSuccessCount()).isEqualTo(1);
+        assertThat(multipleRetrievalStats.loadFailureCount()).isEqualTo(0);
+        assertThat(multipleRetrievalStats.missCount()).isEqualTo(1);
+        assertThat(multipleRetrievalStats.loadCount()).isEqualTo(1);
+    }
+
+    @Test
+    void testNotFoundUser()
+    {
+        SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
+        Map<String, Set<Authorization>> cassandraAuthorizations = 
cassandraAuthorizations();
+        
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
+        Map<String, Set<Authorization>> sidecarAuthorizations = 
sidecarAuthorizations();
+        SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
+        
when(mockSidecarPermissionsAccessor.rolesToAuthorizations()).thenReturn(sidecarAuthorizations);
         SidecarConfiguration mockConfig = mockConfig();
         RoleAuthorizationsCache cache = new RoleAuthorizationsCache(vertx,
                                                                     
executorPools,
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         cache.warmUp(5);
 
         // New entries fetched during refreshes
         assertThat(cache.getAll().size()).isOne();
-        assertThat(cache.getAuthorizations("test_role2")).isNull();
+        assertThat(cache.getAuthorizations("not_found_user")).isNull();
     }
 
     @Test
     void testBulkload() throws InterruptedException
     {
-        Map<String, Set<Authorization>> sidecarAuthorizations = new 
HashMap<>();
-        sidecarAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(BasicPermissions.CREATE_SNAPSHOT.toAuthorization())));
-        sidecarAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(BasicPermissions.STREAM_SNAPSHOT.toAuthorization())));
+        Map<String, Set<Authorization>> sidecarAuthorizations = 
sidecarAuthorizations();
         SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
         
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(sidecarAuthorizations);
         SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
@@ -151,7 +214,8 @@ class RoleAuthorizationsCacheTest
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         // warming cache
@@ -167,9 +231,7 @@ class RoleAuthorizationsCacheTest
     @Test
     void testCacheDisabled()
     {
-        Map<String, Set<Authorization>> sidecarAuthorizations = new 
HashMap<>();
-        sidecarAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(BasicPermissions.CREATE_SNAPSHOT.toAuthorization())));
-        sidecarAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(BasicPermissions.STREAM_SNAPSHOT.toAuthorization())));
+        Map<String, Set<Authorization>> sidecarAuthorizations = 
sidecarAuthorizations();
         SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
         
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(sidecarAuthorizations);
         SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
@@ -184,7 +246,8 @@ class RoleAuthorizationsCacheTest
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAuthorizations("test_role1").size()).isOne();
         assertThat(cache.getAuthorizations("test_role2").size()).isOne();
     }
@@ -201,7 +264,8 @@ class RoleAuthorizationsCacheTest
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         // warming cache
@@ -216,13 +280,10 @@ class RoleAuthorizationsCacheTest
     @Test
     void testSidecarPermissionsNotAddedWhenSchemaDisabled()
     {
-        Map<String, Set<Authorization>> cassandraAuthorizations = new 
HashMap<>();
-        cassandraAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(CassandraPermissions.SELECT.toAuthorization())));
-        cassandraAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(CassandraPermissions.CREATE.toAuthorization())));
+        Map<String, Set<Authorization>> cassandraAuthorizations = 
cassandraAuthorizations();
         SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
         
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
-        Map<String, Set<Authorization>> sidecarAuthorizations = new 
HashMap<>();
-        sidecarAuthorizations.put("test_role3", new 
HashSet<>(Collections.singletonList(BasicPermissions.CREATE_SNAPSHOT.toAuthorization())));
+        Map<String, Set<Authorization>> sidecarAuthorizations = 
sidecarAuthorizations();
         SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
         
when(mockSidecarPermissionsAccessor.rolesToAuthorizations()).thenReturn(sidecarAuthorizations);
         SidecarConfiguration mockConfig = mockConfig();
@@ -233,7 +294,8 @@ class RoleAuthorizationsCacheTest
                                                                     mockConfig,
                                                                     
mockSidecarSchema,
                                                                     
mockDbAccessor,
-                                                                    
mockSidecarPermissionsAccessor);
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         // force warmup of cache
@@ -245,7 +307,135 @@ class RoleAuthorizationsCacheTest
         
assertThat(cache.get("unique_cache_entry_key").get("test_role3")).isNull();
     }
 
+    @Test
+    void testCacheLoadTime()
+    {
+        Map<String, Set<Authorization>> cassandraAuthorizations = 
cassandraAuthorizations();
+        SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
+        
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
+        SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
+
+        SidecarConfiguration mockConfig = mockConfig();
+        RoleAuthorizationsCache cache = new RoleAuthorizationsCache(vertx,
+                                                                    
executorPools,
+                                                                    mockConfig,
+                                                                    
mockSidecarSchema,
+                                                                    
mockDbAccessor,
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
+
+        CacheStats initialStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(initialStats.loadCount()).isZero();
+        assertThat(initialStats.totalLoadTime()).isZero();
+
+        cache.getAuthorizations("test_role1");
+
+        CacheStats afterLoadStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(afterLoadStats.loadCount()).isOne();
+        assertThat(afterLoadStats.totalLoadTime()).isGreaterThan(0);
+
+        double averageLoadTime = afterLoadStats.averageLoadPenalty();
+        assertThat(averageLoadTime).isGreaterThan(0);
+    }
+
+    @Test
+    void testCacheLoadFailureStats()
+    {
+        SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
+        when(mockDbAccessor.findAllRolesAndPermissions()).thenThrow(new 
RuntimeException("Database connection failed"));
+        SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
+
+        SidecarConfiguration mockConfig = mockConfig();
+        RoleAuthorizationsCache cache = new RoleAuthorizationsCache(vertx,
+                                                                    
executorPools,
+                                                                    mockConfig,
+                                                                    
mockSidecarSchema,
+                                                                    
mockDbAccessor,
+                                                                    
mockSidecarPermissionsAccessor,
+                                                                    
sidecarMetrics);
+
+        CacheStats initialStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(initialStats.loadFailureCount()).isZero();
+
+        try
+        {
+            cache.getAuthorizations("test_role1");
+        }
+        catch (Exception e)
+        {
+            // ignore exception
+        }
+
+        CacheStats afterFailureStats = 
sidecarMetrics.server().cache().rolePermissionsCacheMetrics.snapshot();
+        assertThat(afterFailureStats.loadFailureCount()).isEqualTo(1);
+        assertThat(afterFailureStats.loadCount()).isEqualTo(1);
+        assertThat(afterFailureStats.loadSuccessCount()).isEqualTo(0);
+    }
+
+    @Test
+    void testCacheWithInvalidCacheConfig()
+    {
+        Map<String, Set<Authorization>> cassandraAuthorizations = 
cassandraAuthorizations();
+        SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
+        
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
+        SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
+
+        // Configure cache with both expireAfterAccess and refreshAfterWrite 
as null
+        SidecarConfiguration mockConfig = mockConfig(mockCacheConfig(false, 
false, "1s"));
+
+        assertThatThrownBy(
+        () -> new RoleAuthorizationsCache(vertx,
+                                          executorPools,
+                                          mockConfig,
+                                          mockSidecarSchema,
+                                          mockDbAccessor,
+                                          mockSidecarPermissionsAccessor,
+                                          sidecarMetrics))
+        .isInstanceOf(ConfigurationException.class)
+        .hasMessageContaining("role_permissions_cache must be configured with 
either refreshAfterWrite or expireAfterAccess");
+    }
+
+    @Test
+    void testCacheWithOnlyExpireAfterAccess()
+    {
+        Map<String, Set<Authorization>> cassandraAuthorizations = 
cassandraAuthorizations();
+        SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
+        
when(mockDbAccessor.findAllRolesAndPermissions()).thenReturn(cassandraAuthorizations);
+        SidecarPermissionsDatabaseAccessor mockSidecarPermissionsAccessor = 
mock(SidecarPermissionsDatabaseAccessor.class);
+
+        // Configure cache with only expireAfterAccess
+        CacheConfiguration mockCacheConfig = mockCacheConfig(true, false, 
"1s");
+        SidecarConfiguration mockConfig = mockConfig(mockCacheConfig);
+
+        RoleAuthorizationsCache authorizationsCache
+        = new RoleAuthorizationsCache(vertx, executorPools, mockConfig, 
mockSidecarSchema, mockDbAccessor,
+                                      mockSidecarPermissionsAccessor, 
sidecarMetrics);
+        assertThat(authorizationsCache).isNotNull();
+    }
+
+    private CacheConfiguration mockCacheConfig()
+    {
+        return mockCacheConfig(true, true, "1s");
+    }
+
+    private CacheConfiguration mockCacheConfig(boolean setExpire, boolean 
setRefresh, String time)
+    {
+        CacheConfiguration mockCacheConfig = mock(CacheConfiguration.class);
+        when(mockCacheConfig.enabled()).thenReturn(true);
+        when(mockCacheConfig.expireAfterAccess()).thenReturn(setExpire ? 
MillisecondBoundConfiguration.parse("1s") : null);
+        when(mockCacheConfig.refreshAfterWrite()).thenReturn(setRefresh ? 
MillisecondBoundConfiguration.parse("1s") : null);
+        when(mockCacheConfig.maximumSize()).thenReturn(10L);
+        when(mockCacheConfig.warmupRetries()).thenReturn(5);
+        
when(mockCacheConfig.warmupRetryInterval()).thenReturn(MillisecondBoundConfiguration.parse("1s"));
+        return mockCacheConfig;
+    }
+
     private SidecarConfiguration mockConfig()
+    {
+        return mockConfig(mockCacheConfig());
+    }
+
+    private SidecarConfiguration mockConfig(CacheConfiguration 
cacheConfiguration)
     {
         SidecarConfiguration mockConfig = mock(SidecarConfiguration.class);
         ServiceConfiguration mockServiceConfig = 
mock(ServiceConfiguration.class);
@@ -255,13 +445,23 @@ class RoleAuthorizationsCacheTest
         when(mockConfig.serviceConfiguration()).thenReturn(mockServiceConfig);
         AccessControlConfiguration mockAccessControlConfig = 
mock(AccessControlConfiguration.class);
         
when(mockConfig.accessControlConfiguration()).thenReturn(mockAccessControlConfig);
-        CacheConfiguration mockCacheConfig = mock(CacheConfiguration.class);
-        when(mockCacheConfig.enabled()).thenReturn(true);
-        
when(mockCacheConfig.expireAfterAccess()).thenReturn(MillisecondBoundConfiguration.parse("1s"));
-        when(mockCacheConfig.maximumSize()).thenReturn(10L);
-        when(mockCacheConfig.warmupRetries()).thenReturn(5);
-        
when(mockCacheConfig.expireAfterAccess()).thenReturn(MillisecondBoundConfiguration.parse("1s"));
-        
when(mockAccessControlConfig.permissionCacheConfiguration()).thenReturn(mockCacheConfig);
+        
when(mockAccessControlConfig.permissionCacheConfiguration()).thenReturn(cacheConfiguration);
         return mockConfig;
     }
+
+    private Map<String, Set<Authorization>> sidecarAuthorizations()
+    {
+        Map<String, Set<Authorization>> sidecarAuthorizations = new 
HashMap<>();
+        sidecarAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(BasicPermissions.CREATE_SNAPSHOT.toAuthorization())));
+        sidecarAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(BasicPermissions.STREAM_SNAPSHOT.toAuthorization())));
+        return sidecarAuthorizations;
+    }
+
+    private Map<String, Set<Authorization>> cassandraAuthorizations()
+    {
+        Map<String, Set<Authorization>> cassandraAuthorizations = new 
HashMap<>();
+        cassandraAuthorizations.put("test_role1", new 
HashSet<>(Collections.singletonList(CassandraPermissions.SELECT.toAuthorization())));
+        cassandraAuthorizations.put("test_role2", new 
HashSet<>(Collections.singletonList(CassandraPermissions.CREATE.toAuthorization())));
+        return cassandraAuthorizations;
+    }
 }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
index 360d6046..79d2cd2b 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
@@ -66,6 +66,7 @@ import io.vertx.ext.web.handler.impl.ChainAuthHandlerImpl;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.TestModule;
+import 
org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
 import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
 import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration;
@@ -386,7 +387,7 @@ class MutualTLSAuthenticationHandlerTest
                                                  authenticatorsConfiguration(),
                                                  new 
ParameterizedClassConfigurationImpl(className, Collections.emptyMap()),
                                                  
Collections.singleton(ADMIN_IDENTITY),
-                                                 new CacheConfigurationImpl());
+                                                 new 
CacheConfigurationImpl(MillisecondBoundConfiguration.parse("30s"), 100));
 
             return super.abstractConfig(sslConfiguration, builder -> 
builder.accessControlConfiguration(accessControlConfiguration));
         }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
index 66e567da..e88078b1 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
@@ -38,6 +38,9 @@ import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
 import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
@@ -52,14 +55,20 @@ import static org.mockito.Mockito.when;
  */
 class MutualTlsAuthenticationHandlerFactoryTest
 {
+    private static final MetricRegistryFactory FACTORY
+    = new 
MetricRegistryFactory(MutualTlsAuthenticationHandlerFactoryTest.class.getName(),
+                                Collections.emptyList(),
+                                Collections.emptyList());
     Vertx vertx;
     ExecutorPools executorPools;
+    SidecarMetrics sidecarMetrics;
 
     @BeforeEach
     void setup()
     {
         vertx = Vertx.vertx();
         executorPools = createdSharedTestPool(vertx);
+        sidecarMetrics = new SidecarMetricsImpl(FACTORY, null);
     }
 
     @AfterEach
@@ -123,9 +132,9 @@ class MutualTlsAuthenticationHandlerFactoryTest
                                                           
SystemAuthDatabaseAccessor mockAccessor)
     {
         IdentityToRoleCache identityToRoleCache
-        = new IdentityToRoleCache(vertx, executorPools, mockSidecarConfig, 
mockAccessor);
+        = new IdentityToRoleCache(vertx, executorPools, mockSidecarConfig, 
mockAccessor, sidecarMetrics);
         SuperUserCache superUserCache
-        = new SuperUserCache(vertx, executorPools, mockSidecarConfig, 
mockAccessor);
+        = new SuperUserCache(vertx, executorPools, mockSidecarConfig, 
mockAccessor, sidecarMetrics);
         AdminIdentityResolver adminIdentityResolver
         = new AdminIdentityResolver(identityToRoleCache, superUserCache, 
mockSidecarConfig);
         return new MutualTlsAuthenticationHandlerFactory(identityToRoleCache, 
adminIdentityResolver);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCacheTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCacheTest.java
index 9d4730c0..9410299c 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCacheTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authorization/SuperUserCacheTest.java
@@ -36,6 +36,9 @@ import 
org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
+import org.apache.cassandra.sidecar.metrics.SidecarMetricsImpl;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
 import static 
org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SIDECAR_SCHEMA_INITIALIZED;
@@ -49,14 +52,20 @@ import static org.mockito.Mockito.when;
  */
 class SuperUserCacheTest
 {
+    private static final MetricRegistryFactory FACTORY
+    = new MetricRegistryFactory(SuperUserCacheTest.class.getName(),
+                                Collections.emptyList(),
+                                Collections.emptyList());
     Vertx vertx;
     ExecutorPools executorPools;
+    SidecarMetrics sidecarMetrics;
 
     @BeforeEach
     void setup()
     {
         vertx = Vertx.vertx();
         executorPools = createdSharedTestPool(vertx);
+        sidecarMetrics = new SidecarMetricsImpl(FACTORY, null);
     }
 
     @AfterEach
@@ -72,7 +81,7 @@ class SuperUserCacheTest
         
when(mockDbAccessor.findAllRolesToSuperuserStatus()).thenReturn(ImmutableMap.of("test_role1",
 true,
                                                                                
         "test_role2", false));
         SidecarConfiguration mockConfig = mockConfig();
-        SuperUserCache cache = new SuperUserCache(vertx, executorPools, 
mockConfig, mockDbAccessor);
+        SuperUserCache cache = new SuperUserCache(vertx, executorPools, 
mockConfig, mockDbAccessor, sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         // warming cache
@@ -97,7 +106,7 @@ class SuperUserCacheTest
         
when(mockDbAccessor.findAllRolesToSuperuserStatus()).thenReturn(superUserMap);
         SidecarConfiguration mockConfig = mockConfig();
         
when(mockConfig.accessControlConfiguration().permissionCacheConfiguration().enabled()).thenReturn(false);
-        SuperUserCache superUserCache = new SuperUserCache(vertx, 
executorPools, mockConfig, mockDbAccessor);
+        SuperUserCache superUserCache = new SuperUserCache(vertx, 
executorPools, mockConfig, mockDbAccessor, sidecarMetrics);
         assertThat(superUserCache.get("test_role")).isTrue();
         assertThat(superUserCache.isSuperUser("test_role")).isTrue();
         assertThat(superUserCache.getAll().size()).isEqualTo(2);
@@ -109,7 +118,7 @@ class SuperUserCacheTest
         SystemAuthDatabaseAccessor mockDbAccessor = 
mock(SystemAuthDatabaseAccessor.class);
         
when(mockDbAccessor.findAllRolesToSuperuserStatus()).thenReturn(Collections.emptyMap());
         SidecarConfiguration mockConfig = mockConfig();
-        SuperUserCache cache = new SuperUserCache(vertx, executorPools, 
mockConfig, mockDbAccessor);
+        SuperUserCache cache = new SuperUserCache(vertx, executorPools, 
mockConfig, mockDbAccessor, sidecarMetrics);
         assertThat(cache.getAll().size()).isZero();
 
         // warming cache
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
index a16da2b3..45e763b9 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
@@ -418,6 +418,7 @@ class SidecarConfigurationTest
         assertThat(permissionCacheConfiguration.enabled()).isTrue();
         
assertThat(permissionCacheConfiguration.expireAfterAccess().quantity()).isEqualTo(5);
         
assertThat(permissionCacheConfiguration.expireAfterAccess().unit()).isEqualTo(TimeUnit.MINUTES);
+        assertThat(permissionCacheConfiguration.refreshAfterWrite()).isNull();
         assertThat(permissionCacheConfiguration.maximumSize()).isEqualTo(1000);
         assertThat(permissionCacheConfiguration.warmupRetries()).isEqualTo(5);
         
assertThat(permissionCacheConfiguration.warmupRetryInterval().quantity()).isEqualTo(2);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
new file mode 100644
index 00000000..ff16f9cd
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/config/yaml/CacheConfigurationImplTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.cassandra.sidecar.config.yaml;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import 
org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test for {@link CacheConfigurationImpl}
+ */
+class CacheConfigurationImplTest
+{
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Test
+    void testDefaultConstructor()
+    {
+        CacheConfigurationImpl config = new CacheConfigurationImpl();
+
+        assertThat(config.expireAfterAccess()).isNull();
+        assertThat(config.refreshAfterWrite()).isNull();
+        assertThat(config.maximumSize()).isEqualTo(100);
+        assertThat(config.enabled()).isTrue();
+        assertThat(config.warmupRetries()).isEqualTo(5);
+        
assertThat(config.warmupRetryInterval()).isEqualTo(MillisecondBoundConfiguration.parse("1s"));
+    }
+
+    @Test
+    void testConstructorWithParameters()
+    {
+        MillisecondBoundConfiguration expireAfterAccess = 
MillisecondBoundConfiguration.parse("30m");
+        MillisecondBoundConfiguration refreshAfterWrite = 
MillisecondBoundConfiguration.parse("5m");
+        MillisecondBoundConfiguration warmupRetryInterval = 
MillisecondBoundConfiguration.parse("2s");
+
+        CacheConfigurationImpl config
+        = new CacheConfigurationImpl(expireAfterAccess, refreshAfterWrite, 
1000, false,
+                                     10, warmupRetryInterval);
+
+        assertThat(config.expireAfterAccess()).isEqualTo(expireAfterAccess);
+        assertThat(config.refreshAfterWrite()).isEqualTo(refreshAfterWrite);
+        assertThat(config.maximumSize()).isEqualTo(1000);
+        assertThat(config.enabled()).isFalse();
+        assertThat(config.warmupRetries()).isEqualTo(10);
+        
assertThat(config.warmupRetryInterval()).isEqualTo(warmupRetryInterval);
+    }
+
+    @Test
+    void testBuilderWithDefaults()
+    {
+        CacheConfigurationImpl config = new 
CacheConfigurationImpl(MillisecondBoundConfiguration.parse("30m"), 100);
+
+        
assertThat(config.expireAfterAccess()).isEqualTo(MillisecondBoundConfiguration.parse("30m"));
+        
assertThat(config.refreshAfterWrite()).isEqualTo(MillisecondBoundConfiguration.parse("1h"));
+        assertThat(config.maximumSize()).isEqualTo(100);
+        assertThat(config.enabled()).isTrue();
+        assertThat(config.warmupRetries()).isEqualTo(5);
+        
assertThat(config.warmupRetryInterval()).isEqualTo(MillisecondBoundConfiguration.parse("1s"));
+    }
+
+    @Test
+    void testSerializationWithOnlyRefreshAfterWrite() throws Exception
+    {
+        String jsonString = "{" +
+                            "\"enabled\": \"true\"," +
+                            "\"refresh_after_write\": \"5m\"," +
+                            "\"maximum_size\": 1000," +
+                            "\"warmup_retries\": 10," +
+                            "\"warmup_retry_interval\": \"2s\"" +
+                            "}";
+
+        CacheConfigurationImpl config = MAPPER.readValue(jsonString, 
CacheConfigurationImpl.class);
+
+        assertThat(config.enabled()).isTrue();
+        assertThat(config.expireAfterAccess()).isNull();
+        
assertThat(config.refreshAfterWrite()).isEqualTo(MillisecondBoundConfiguration.parse("5m"));
+        assertThat(config.maximumSize()).isEqualTo(1000);
+        assertThat(config.warmupRetries()).isEqualTo(10);
+        
assertThat(config.warmupRetryInterval()).isEqualTo(MillisecondBoundConfiguration.parse("2s"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to