This is an automated email from the ASF dual-hosted git repository. samt pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/cassandra.git
commit d0a207b41441823d77b69c62556e6c5d2d4ad88e Merge: a339aa9 8f33dc0 Author: Sam Tunnicliffe <[email protected]> AuthorDate: Fri Jul 5 19:06:09 2019 +0100 Merge branch 'cassandra-3.11' into trunk CHANGES.txt | 1 + .../apache/cassandra/auth/CassandraAuthorizer.java | 26 ++- .../cassandra/auth/CassandraRoleManager.java | 20 +- .../cassandra/auth/PasswordAuthenticator.java | 42 ++-- src/java/org/apache/cassandra/auth/Roles.java | 43 +++- .../cassandra/auth/jmx/AuthorizationProxy.java | 6 - .../exceptions/AuthenticationException.java | 5 + .../exceptions/UnauthorizedException.java | 5 + .../org/apache/cassandra/service/ClientState.java | 16 +- .../org/apache/cassandra/auth/AuthCacheTest.java | 254 ++++++++++++++------- 10 files changed, 293 insertions(+), 125 deletions(-) diff --cc src/java/org/apache/cassandra/auth/CassandraAuthorizer.java index 238b5b5,d4253d1..37ad60a --- a/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java +++ b/src/java/org/apache/cassandra/auth/CassandraAuthorizer.java @@@ -53,7 -63,12 +53,7 @@@ public class CassandraAuthorizer implem private static final String RESOURCE = "resource"; private static final String PERMISSIONS = "permissions"; - SelectStatement authorizeRoleStatement; - // used during upgrades to perform authz on mixed clusters - public static final String USERNAME = "username"; - public static final String USER_PERMISSIONS = "permissions"; - + private SelectStatement authorizeRoleStatement; - private SelectStatement legacyAuthorizeRoleStatement; public CassandraAuthorizer() { @@@ -63,16 -78,23 +63,24 @@@ // or indirectly via roles granted to the user. public Set<Permission> authorize(AuthenticatedUser user, IResource resource) { - if (user.isSuper()) - return resource.applicablePermissions(); + try + { + if (user.isSuper()) + return resource.applicablePermissions(); - Set<Permission> permissions = EnumSet.noneOf(Permission.class); + Set<Permission> permissions = EnumSet.noneOf(Permission.class); - // Even though we only care about the RoleResource here, we use getRoleDetails as - // it saves a Set creation in RolesCache - for (Role role: user.getRoleDetails()) - addPermissionsForRole(permissions, resource, role.resource); - return permissions; - for (RoleResource role: user.getRoles()) - addPermissionsForRole(permissions, resource, role); - ++ // Even though we only care about the RoleResource here, we use getRoleDetails as ++ // it saves a Set creation in RolesCache ++ for (Role role: user.getRoleDetails()) ++ addPermissionsForRole(permissions, resource, role.resource); + return permissions; + } + catch (RequestExecutionException | RequestValidationException e) + { + logger.debug("Failed to authorize {} for {}", user, resource); + throw new UnauthorizedException("Unable to perform authorization of permissions: " + e.getMessage(), e); + } } public void grant(AuthenticatedUser performer, Set<Permission> permissions, IResource resource, RoleResource grantee) diff --cc src/java/org/apache/cassandra/auth/PasswordAuthenticator.java index 27a68a0,b1a7227..89f765d --- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java @@@ -29,7 -29,8 +29,8 @@@ import org.slf4j.Logger import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.config.Schema; -import org.apache.cassandra.config.SchemaConstants; ++import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; @@@ -98,27 -103,55 +99,34 @@@ public class PasswordAuthenticator impl return new AuthenticatedUser(username); } - private String queryHashedPassword(String username) throws AuthenticationException + private String queryHashedPassword(String username) { - ResultMessage.Rows rows = - authenticateStatement.execute(QueryState.forInternalCalls(), - QueryOptions.forInternalCalls(consistencyForRole(username), - Lists.newArrayList(ByteBufferUtil.bytes(username))), - System.nanoTime()); - - // If either a non-existent role name was supplied, or no credentials - // were found for that role we don't want to cache the result so we throw - // a specific, but unchecked, exception to keep LoadingCache happy. - if (rows.result.isEmpty()) - throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username)); - - UntypedResultSet result = UntypedResultSet.create(rows.result); - if (!result.one().has(SALTED_HASH)) - throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username)); - - return result.one().getString(SALTED_HASH); + try + { - SelectStatement authenticationStatement = authenticationStatement(); - + ResultMessage.Rows rows = - authenticationStatement.execute(QueryState.forInternalCalls(), - QueryOptions.forInternalCalls(consistencyForRole(username), - Lists.newArrayList(ByteBufferUtil.bytes(username))), - System.nanoTime()); ++ authenticateStatement.execute(QueryState.forInternalCalls(), ++ QueryOptions.forInternalCalls(consistencyForRole(username), ++ Lists.newArrayList(ByteBufferUtil.bytes(username))), ++ System.nanoTime()); + + // If either a non-existent role name was supplied, or no credentials + // were found for that role we don't want to cache the result so we throw + // an exception. + if (rows.result.isEmpty()) + throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username)); + + UntypedResultSet result = UntypedResultSet.create(rows.result); + if (!result.one().has(SALTED_HASH)) + throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username)); + + return result.one().getString(SALTED_HASH); + } + catch (RequestExecutionException e) + { + throw new AuthenticationException("Unable to perform authentication: " + e.getMessage(), e); + } } - /** - * If the legacy users table exists try to verify credentials there. This is to handle the case - * where the cluster is being upgraded and so is running with mixed versions of the authn tables - */ - private SelectStatement authenticationStatement() - { - if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, LEGACY_CREDENTIALS_TABLE) == null) - return authenticateStatement; - else - { - // the statement got prepared, we to try preparing it again. - // If the credentials was initialised only after statement got prepared, re-prepare (CASSANDRA-12813). - if (legacyAuthenticateStatement == null) - prepareLegacyAuthenticateStatement(); - return legacyAuthenticateStatement; - } - } - - public Set<DataResource> protectedResources() { // Also protected by CassandraRoleManager, but the duplication doesn't hurt and is more explicit diff --cc src/java/org/apache/cassandra/auth/Roles.java index 22eb0d3,2b1ff6e..527451e --- a/src/java/org/apache/cassandra/auth/Roles.java +++ b/src/java/org/apache/cassandra/auth/Roles.java @@@ -17,46 -17,25 +17,53 @@@ */ package org.apache.cassandra.auth; +import java.util.Collections; +import java.util.Map; import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.apache.cassandra.config.DatabaseDescriptor; + import org.apache.cassandra.exceptions.RequestExecutionException; + import org.apache.cassandra.exceptions.UnauthorizedException; public class Roles { + private static final Logger logger = LoggerFactory.getLogger(Roles.class); + - private static final RolesCache cache = new RolesCache(DatabaseDescriptor.getRoleManager()); + private static final Role NO_ROLE = new Role("", false, false, Collections.emptyMap(), Collections.emptySet()); + + private static RolesCache cache; + static + { + initRolesCache(DatabaseDescriptor.getRoleManager(), + () -> DatabaseDescriptor.getAuthenticator().requireAuthentication()); + } + + @VisibleForTesting + public static void initRolesCache(IRoleManager roleManager, BooleanSupplier enableCache) + { + if (cache != null) + cache.unregisterMBean(); + cache = new RolesCache(roleManager, enableCache); + } + + @VisibleForTesting + public static void clearCache() + { + cache.invalidate(); + } /** - * Get all roles granted to the supplied Role, including both directly granted + * Identify all roles granted to the supplied Role, including both directly granted * and inherited roles. - * The returned roles may be cached if {@code roles_validity_in_ms > 0} + * This method is used where we mainly just care about *which* roles are granted to a given role, + * including when looking up or listing permissions for a role on a given resource. * * @param primaryRole the Role * @return set of all granted Roles for the primary Role @@@ -91,86 -54,18 +98,102 @@@ */ public static boolean hasSuperuserStatus(RoleResource role) { - for (Role r : getRoleDetails(role)) - if (r.isSuper) - return true; + try + { - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); - for (RoleResource r : cache.getRoles(role)) - if (roleManager.isSuper(r)) ++ for (Role r : getRoleDetails(role)) ++ if (r.isSuper) + return true; + - return false; + return false; + } + catch (RequestExecutionException e) + { + logger.debug("Failed to authorize {} for super-user permission", role.getRoleName()); + throw new UnauthorizedException("Unable to perform authorization of super-user permission: " + e.getMessage(), e); + } } + + /** + * Returns true if the supplied role has the login privilege. This cannot be inherited, so + * returns true iff the named role has that bit set. + * @param role the role identifier + * @return true if the role has the canLogin privilege, false otherwise + */ + public static boolean canLogin(final RoleResource role) + { - for (Role r : getRoleDetails(role)) - if (r.resource.equals(role)) - return r.canLogin; ++ try ++ { ++ for (Role r : getRoleDetails(role)) ++ if (r.resource.equals(role)) ++ return r.canLogin; + - return false; ++ return false; ++ } ++ catch (RequestExecutionException e) ++ { ++ logger.debug("Failed to authorize {} for login permission", role.getRoleName()); ++ throw new UnauthorizedException("Unable to perform authorization of login permission: " + e.getMessage(), e); ++ } + } + + /** + * Returns the map of custom options for the named role. These options are not inherited from granted roles, but + * are set directly. + * @param role the role identifier + * @return map of option_name -> value. If no options are set for the named role, the map will be empty + * but never null. + */ + public static Map<String, String> getOptions(RoleResource role) + { + for (Role r : getRoleDetails(role)) + if (r.resource.equals(role)) + return r.options; + + return NO_ROLE.options; + } + + /** + * Return the NullObject Role instance which can be safely used to indicate no information is available + * when querying for a specific named role. + * @return singleton null role object + */ + public static Role nullRole() + { + return NO_ROLE; + } + + /** + * Just a convenience method which compares a role instance with the null object version, indicating if the + * return from some query/lookup method was a valid Role or indicates that the role does not exist. + * @param role + * @return true if the supplied role is the null role instance, false otherwise. + */ + public static boolean isNullRole(Role role) + { + return NO_ROLE.equals(role); + } + + + /** + * Constructs a Role object from a RoleResource, using the methods of the supplied IRoleManager. + * This is used by the default implementation of IRoleManager#getRoleDetails so that IRoleManager impls + * which don't implement an optimized getRoleDetails remain compatible. Depending on the IRoleManager + * implementation this could be quite heavyweight, so should not be used on any hot path. + * + * @param resource identifies the role + * @param roleManager provides lookup functions to retrieve role info + * @return Role object including superuser status, login privilege, custom options and the set of roles + * granted to identified role. + */ + public static Role fromRoleResource(RoleResource resource, IRoleManager roleManager) + { + return new Role(resource.getName(), + roleManager.isSuper(resource), + roleManager.canLogin(resource), + roleManager.getCustomOptions(resource), + roleManager.getRoles(resource, false) + .stream() + .map(RoleResource::getRoleName) + .collect(Collectors.toSet())); + } } diff --cc src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java index ef00027,7bfbf52..b213c43 --- a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java +++ b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java @@@ -33,7 -32,7 +33,6 @@@ import javax.management.ObjectName import javax.security.auth.Subject; import com.google.common.annotations.VisibleForTesting; --import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --cc src/java/org/apache/cassandra/service/ClientState.java index 26ed271,15262d7..81574e6 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@@ -30,12 -28,10 +30,14 @@@ import org.slf4j.Logger import org.slf4j.LoggerFactory; import org.apache.cassandra.auth.*; -import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace; ++import org.apache.cassandra.exceptions.RequestExecutionException; ++import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.config.Schema; -import org.apache.cassandra.config.SchemaConstants; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.cql3.QueryHandler; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.functions.Function; @@@ -321,15 -264,38 +323,27 @@@ public class ClientStat /** * Attempts to login the given user. */ - public void login(AuthenticatedUser user) throws AuthenticationException + public void login(AuthenticatedUser user) { - if (user.isAnonymous() || user.canLogin()) - // Login privilege is not inherited via granted roles, so just - // verify that the role with the credentials that were actually - // supplied has it + if (user.isAnonymous() || canLogin(user)) this.user = user; else throw new AuthenticationException(String.format("%s is not permitted to log in", user.getName())); } + private boolean canLogin(AuthenticatedUser user) + { + try + { - return DatabaseDescriptor.getRoleManager().canLogin(user.getPrimaryRole()); - } catch (RequestExecutionException e) { ++ return user.canLogin(); ++ } ++ catch (RequestExecutionException | RequestValidationException e) ++ { + throw new AuthenticationException("Unable to perform authentication: " + e.getMessage(), e); + } + } + - public void hasAllKeyspacesAccess(Permission perm) throws UnauthorizedException + public void ensureAllKeyspacesPermission(Permission perm) { if (isInternal) return; diff --cc test/unit/org/apache/cassandra/auth/AuthCacheTest.java index cc78ebc,0030603..217821e --- a/test/unit/org/apache/cassandra/auth/AuthCacheTest.java +++ b/test/unit/org/apache/cassandra/auth/AuthCacheTest.java @@@ -18,16 -17,16 +17,20 @@@ */ package org.apache.cassandra.auth; - import org.junit.Assert; - import org.junit.BeforeClass; -import java.util.function.Consumer; ++import java.util.function.BooleanSupplier; + import java.util.function.Function; -import java.util.function.Supplier; ++import java.util.function.IntConsumer; ++import java.util.function.IntSupplier; + import org.junit.Test; - import org.apache.cassandra.config.DatabaseDescriptor; + import org.apache.cassandra.db.ConsistencyLevel; + import org.apache.cassandra.exceptions.UnavailableException; - import static org.junit.Assert.assertFalse; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class AuthCacheTest { @@@ -41,97 -46,119 +50,182 @@@ } @Test - public void testCaching() - { - AuthCache<String, String> authCache = new AuthCache<>("TestCache", - DatabaseDescriptor::setCredentialsValidity, - DatabaseDescriptor::getCredentialsValidity, - DatabaseDescriptor::setCredentialsUpdateInterval, - DatabaseDescriptor::getCredentialsUpdateInterval, - DatabaseDescriptor::setCredentialsCacheMaxEntries, - DatabaseDescriptor::getCredentialsCacheMaxEntries, - this::load, - () -> true - ); - - // Test cacheloader is called if set - loadFuncCalled = false; - String result = authCache.get("test"); - assertTrue(loadFuncCalled); - Assert.assertEquals("load", result); - - // value should be fetched from cache - loadFuncCalled = false; - String result2 = authCache.get("test"); - assertFalse(loadFuncCalled); - Assert.assertEquals("load", result2); - - // value should be fetched from cache after complete invalidate + public void testCacheLoaderIsNotCalledOnSecond() + { + TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + authCache.get("10"); + assertEquals(1, loadCounter); + + int result = authCache.get("10"); + + assertEquals(10, result); + assertEquals(1, loadCounter); + } + + @Test + public void testCacheLoaderIsAlwaysCalledWhenDisabled() + { + isCacheEnabled = false; + TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + + authCache.get("10"); + int result = authCache.get("10"); + + assertEquals(10, result); + assertEquals(2, loadCounter); + } + + @Test + public void testCacheLoaderIsAlwaysCalledWhenValidityIsZero() + { + setValidity(0); + TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + + authCache.get("10"); + int result = authCache.get("10"); + + assertEquals(10, result); + assertEquals(2, loadCounter); + } + + @Test + public void testCacheLoaderIsCalledAfterFullInvalidate() + { + TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + authCache.get("10"); + authCache.invalidate(); - loadFuncCalled = false; - String result3 = authCache.get("test"); - assertTrue(loadFuncCalled); - Assert.assertEquals("load", result3); - - // value should be fetched from cache after invalidating key - authCache.invalidate("test"); - loadFuncCalled = false; - String result4 = authCache.get("test"); - assertTrue(loadFuncCalled); - Assert.assertEquals("load", result4); - - // set cache to null and load function should be called - loadFuncCalled = false; + int result = authCache.get("10"); + + assertEquals(10, result); + assertEquals(2, loadCounter); + } + + @Test + public void testCacheLoaderIsCalledAfterInvalidateKey() + { + TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + authCache.get("10"); + + authCache.invalidate("10"); + int result = authCache.get("10"); + + assertEquals(10, result); + assertEquals(2, loadCounter); + } + ++ @Test ++ public void testCacheLoaderIsCalledAfterReset() ++ { ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); ++ authCache.get("10"); ++ + authCache.cache = null; - String result5 = authCache.get("test"); - assertTrue(loadFuncCalled); - Assert.assertEquals("load", result5); ++ int result = authCache.get("10"); ++ ++ assertEquals(10, result); ++ assertEquals(2, loadCounter); + } + + @Test - public void testInitCache() - { - // Test that a validity of <= 0 will turn off caching - DatabaseDescriptor.setCredentialsValidity(0); - AuthCache<String, String> authCache = new AuthCache<>("TestCache2", - DatabaseDescriptor::setCredentialsValidity, - DatabaseDescriptor::getCredentialsValidity, - DatabaseDescriptor::setCredentialsUpdateInterval, - DatabaseDescriptor::getCredentialsUpdateInterval, - DatabaseDescriptor::setCredentialsCacheMaxEntries, - DatabaseDescriptor::getCredentialsCacheMaxEntries, - this::load, - () -> true); ++ public void testThatZeroValidityTurnOffCaching() ++ { ++ setValidity(0); ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); ++ authCache.get("10"); ++ int result = authCache.get("10"); ++ + assertNull(authCache.cache); ++ assertEquals(10, result); ++ assertEquals(2, loadCounter); ++ } ++ ++ @Test ++ public void testThatRaisingValidityTurnOnCaching() ++ { ++ setValidity(0); ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); ++ + authCache.setValidity(2000); + authCache.cache = authCache.initCache(null); ++ + assertNotNull(authCache.cache); ++ } ++ ++ @Test ++ public void testDisableCache() ++ { ++ isCacheEnabled = false; ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + - // Test enableCache works as intended - authCache = new AuthCache<>("TestCache3", - DatabaseDescriptor::setCredentialsValidity, - DatabaseDescriptor::getCredentialsValidity, - DatabaseDescriptor::setCredentialsUpdateInterval, - DatabaseDescriptor::getCredentialsUpdateInterval, - DatabaseDescriptor::setCredentialsCacheMaxEntries, - DatabaseDescriptor::getCredentialsCacheMaxEntries, - this::load, - () -> isCacheEnabled); + assertNull(authCache.cache); ++ } ++ ++ @Test ++ public void testDynamicallyEnableCache() ++ { ++ isCacheEnabled = false; ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); ++ + isCacheEnabled = true; + authCache.cache = authCache.initCache(null); ++ + assertNotNull(authCache.cache); ++ } ++ ++ @Test ++ public void testDefaultPolicies() ++ { ++ TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled); + - // Ensure at a minimum these policies have been initialised by default + assertTrue(authCache.cache.policy().expireAfterWrite().isPresent()); + assertTrue(authCache.cache.policy().refreshAfterWrite().isPresent()); + assertTrue(authCache.cache.policy().eviction().isPresent()); + } + - private String load(String test) + @Test(expected = UnavailableException.class) + public void testCassandraExceptionPassThroughWhenCacheEnabled() + { - TestCache<String, Integer> cache = new TestCache<>(s -> { - throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1); - }, this::setValidity, () -> validity, () -> isCacheEnabled); ++ TestCache<String, Integer> cache = new TestCache<>(s -> { throw UnavailableException.create(ConsistencyLevel.QUORUM, 3, 1); }, this::setValidity, () -> validity, () -> isCacheEnabled); + + cache.get("expect-exception"); + } + + @Test(expected = UnavailableException.class) + public void testCassandraExceptionPassThroughWhenCacheDisable() { - loadFuncCalled = true; - return "load"; + isCacheEnabled = false; - TestCache<String, Integer> cache = new TestCache<>(s -> { - throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1); - }, this::setValidity, () -> validity, () -> isCacheEnabled); ++ TestCache<String, Integer> cache = new TestCache<>(s -> { throw UnavailableException.create(ConsistencyLevel.QUORUM, 3, 1); }, this::setValidity, () -> validity, () -> isCacheEnabled); + + cache.get("expect-exception"); } + private void setValidity(int validity) + { + this.validity = validity; + } + + private Integer countingLoader(String s) + { + loadCounter++; + return Integer.parseInt(s); + } + + private static class TestCache<K, V> extends AuthCache<K, V> + { + private static int nameCounter = 0; // Allow us to create many instances of cache with same name prefix + - TestCache(Function<K, V> loadFunction, Consumer<Integer> setValidityDelegate, Supplier<Integer> getValidityDelegate, Supplier<Boolean> cacheEnabledDelegate) ++ TestCache(Function<K, V> loadFunction, IntConsumer setValidityDelegate, IntSupplier getValidityDelegate, BooleanSupplier cacheEnabledDelegate) + { + super("TestCache" + nameCounter++, + setValidityDelegate, + getValidityDelegate, - (updateInterval) -> { - }, ++ (updateInterval) -> {}, + () -> 1000, - (maxEntries) -> { - }, ++ (maxEntries) -> {}, + () -> 10, + loadFunction, + cacheEnabledDelegate); + } + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
