blob: 50a0a9c0fc7f88c4cb784689c45035fbc65b7d9f [file] [log] [blame]
/*
* 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.auth;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import org.junit.Test;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.UnavailableException;
import static org.apache.cassandra.config.CassandraRelevantProperties.AUTH_CACHE_WARMING_MAX_RETRIES;
import static org.apache.cassandra.config.CassandraRelevantProperties.AUTH_CACHE_WARMING_RETRY_INTERVAL_MS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class AuthCacheTest
{
private int loadCounter = 0;
private int validity = 2000;
private boolean isCacheEnabled = true;
private final int MAX_ENTRIES = 10;
@Test
public void testCacheLoaderIsCalledOnFirst()
{
TestCache authCache = newCache();
assertEquals(10, (int)authCache.get("10"));
assertEquals(1, loadCounter);
}
@Test
public void testCacheLoaderIsNotCalledOnSecond()
{
TestCache authCache = newCache();
authCache.get("10");
assertEquals(1, loadCounter);
assertEquals(10, (int)authCache.get("10"));
assertEquals(1, loadCounter);
}
@Test
public void testCacheLoaderIsAlwaysCalledWhenDisabled()
{
isCacheEnabled = false;
TestCache authCache = newCache();
authCache.get("10");
assertEquals(10, (int)authCache.get("10"));
assertEquals(2, loadCounter);
}
@Test
public void testCacheLoaderIsAlwaysCalledWhenValidityIsZero()
{
setValidity(0);
TestCache authCache = newCache();
authCache.get("10");
assertEquals(10, (int)authCache.get("10"));
assertEquals(2, loadCounter);
}
@Test
public void testCacheLoaderIsCalledAfterFullInvalidate()
{
TestCache authCache = newCache();
authCache.get("10");
authCache.get("11");
assertEquals(2, loadCounter);
authCache.invalidate();
assertEquals(10, (int)authCache.get("10"));
assertEquals(11, (int)authCache.get("11"));
assertEquals(4, loadCounter);
}
@Test
public void testCacheLoaderIsCalledAfterInvalidateKey()
{
TestCache authCache = newCache();
authCache.get("10");
authCache.get("11"); // second key that should not be invalidated
assertEquals(2, loadCounter);
authCache.invalidate("10");
assertEquals(10, (int)authCache.get("10"));
assertEquals(11, (int)authCache.get("11"));
assertEquals(3, loadCounter);
}
@Test
public void testCacheLoaderIsCalledAfterReset()
{
TestCache authCache = newCache();
authCache.get("10");
authCache.cache = null;
int result = authCache.get("10");
assertEquals(10, result);
assertEquals(2, loadCounter);
}
@Test
public void testThatZeroValidityTurnOffCaching()
{
setValidity(0);
TestCache authCache = newCache();
authCache.get("10");
int result = authCache.get("10");
assertNull(authCache.cache);
assertEquals(10, result);
assertEquals(2, loadCounter);
}
@Test
public void testThatRaisingValidityTurnOnCaching()
{
setValidity(0);
TestCache authCache = newCache();
authCache.setValidity(2000);
authCache.cache = authCache.initCache(null);
assertNotNull(authCache.cache);
}
@Test
public void testDisableCache()
{
isCacheEnabled = false;
TestCache authCache = newCache();
assertNull(authCache.cache);
}
@Test
public void testDynamicallyEnableCache()
{
isCacheEnabled = false;
TestCache authCache = newCache();
isCacheEnabled = true;
authCache.cache = authCache.initCache(null);
assertNotNull(authCache.cache);
}
@Test
public void testDefaultPolicies()
{
TestCache authCache = newCache();
assertTrue(authCache.cache.policy().expireAfterWrite().isPresent());
assertTrue(authCache.cache.policy().refreshAfterWrite().isPresent());
assertTrue(authCache.cache.policy().eviction().isPresent());
}
@Test(expected = UnavailableException.class)
public void testCassandraExceptionPassThroughWhenCacheEnabled()
{
TestCache cache = newCache(s -> { throw UnavailableException.create(ConsistencyLevel.QUORUM, 3, 1); });
cache.get("expect-exception");
}
@Test(expected = UnavailableException.class)
public void testCassandraExceptionPassThroughWhenCacheDisable()
{
isCacheEnabled = false;
TestCache cache = newCache(s -> { throw UnavailableException.create(ConsistencyLevel.QUORUM, 3, 1); });
cache.get("expect-exception");
}
@Test
public void testCassandraExceptionPassThroughWhenCacheRefreshed() throws InterruptedException
{
setValidity(50);
TestCache cache = new TestCache(this::countingLoaderWithException, this::emptyBulkLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
cache.get("10");
// wait until the cached record expires
Thread.sleep(60);
for (int i = 1; i <= 5; i++)
{
try
{
cache.get("10");
fail("Did not get expected Exception on attempt " + i);
}
catch (UnavailableException expected)
{
}
}
}
@Test
public void warmCacheUsingEntryProvider()
{
AtomicBoolean provided = new AtomicBoolean(false);
Supplier<Map<String, Integer>> bulkLoader = () -> {
provided.set(true);
return Collections.singletonMap("0", 0);
};
TestCache cache = newCache(bulkLoader);
cache.warm();
assertEquals(1, cache.getEstimatedSize());
assertEquals(0, (int)cache.get("0")); // warmed entry
assertEquals(0, loadCounter);
assertEquals(10, (int)cache.get("10")); // cold entry
assertEquals(1, loadCounter);
assertTrue(provided.get());
}
@Test
public void warmCacheIsSafeIfCachingIsDisabled()
{
isCacheEnabled = false;
TestCache cache = newCache(() -> Collections.singletonMap("0", 0));
cache.warm();
assertEquals(0, cache.getEstimatedSize());
}
@Test
public void providerSuppliesMoreEntriesThanCapacity()
{
Supplier<Map<String, Integer>> bulkLoader = () -> {
Map<String, Integer> entries = new HashMap<>();
for (int i = 0; i < MAX_ENTRIES * 2; i++)
entries.put(Integer.toString(i), i);
return entries;
};
TestCache cache = new TestCache(this::countingLoader,
bulkLoader,
this::setValidity,
() -> validity,
() -> isCacheEnabled);
cache.warm();
cache.cleanup(); // Force the cleanup task rather than waiting for it to be scheduled to get accurate count
assertEquals(MAX_ENTRIES, cache.getEstimatedSize());
}
@Test
public void handleProviderErrorDuringWarming()
{
AUTH_CACHE_WARMING_MAX_RETRIES.setInt(3);
AUTH_CACHE_WARMING_RETRY_INTERVAL_MS.setLong(0);
final AtomicInteger attempts = new AtomicInteger(0);
Supplier<Map<String, Integer>> bulkLoader = () -> {
if (attempts.incrementAndGet() < 3)
throw new RuntimeException("BOOM");
return Collections.singletonMap("0", 99);
};
TestCache cache = newCache(bulkLoader);
cache.warm();
assertEquals(1, cache.getEstimatedSize());
assertEquals(99, (int)cache.get("0"));
// We should have made 3 attempts to get the initial entries
assertEquals(3, attempts.get());
}
@Test
public void testCacheLoaderIsNotCalledOnGetAllWhenCacheIsDisabled()
{
isCacheEnabled = false;
TestCache authCache = new TestCache(this::countingLoader, this::emptyBulkLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
authCache.get("10");
Map<String, Integer> result = authCache.getAll();
// even though the cache is disabled and nothing is cache we still use loadFunction on get operation, so
// its counter has been incremented
assertThat(result).isEmpty();
assertEquals(1, loadCounter);
}
@Test
public void testCacheLoaderIsNotCalledOnGetAllWhenCacheIsEmpty()
{
TestCache authCache = new TestCache(this::countingLoader, this::emptyBulkLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
Map<String, Integer> result = authCache.getAll();
assertThat(result).isEmpty();
assertEquals(0, loadCounter);
}
@Test
public void testCacheLoaderIsNotCalledOnGetAllWhenCacheIsNotEmpty()
{
TestCache authCache = new TestCache(this::countingLoader, this::emptyBulkLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
authCache.get("10");
Map<String, Integer> result = authCache.getAll();
assertThat(result).hasSize(1);
assertThat(result).containsEntry("10", 10);
assertEquals(1, loadCounter);
}
private void setValidity(int validity)
{
this.validity = validity;
}
private Integer countingLoader(String s)
{
loadCounter++;
return Integer.parseInt(s);
}
private Integer countingLoaderWithException(String s)
{
Integer loadedValue = countingLoader(s);
if (loadCounter > 1)
throw UnavailableException.create(ConsistencyLevel.QUORUM, 3, 1);
return loadedValue;
}
private Map<String, Integer> emptyBulkLoader()
{
return Collections.emptyMap();
}
private TestCache newCache()
{
return new TestCache(this::countingLoader,
this::emptyBulkLoader,
this::setValidity,
() -> validity,
() -> isCacheEnabled);
}
private TestCache newCache(Function<String, Integer> loadFunction)
{
return new TestCache(loadFunction,
this::emptyBulkLoader,
this::setValidity,
() -> validity,
() -> isCacheEnabled);
}
private TestCache newCache(Supplier<Map<String, Integer>> bulkLoader)
{
return new TestCache(this::countingLoader,
bulkLoader,
this::setValidity,
() -> validity,
() -> isCacheEnabled);
}
private static class TestCache extends AuthCache<String, Integer>
{
private static int nameCounter = 0; // Allow us to create many instances of cache with same name prefix
TestCache(Function<String, Integer> loadFunction,
Supplier<Map<String, Integer>> bulkLoadFunction,
IntConsumer setValidityDelegate,
IntSupplier getValidityDelegate,
BooleanSupplier cacheEnabledDelegate)
{
super("TestCache" + nameCounter++,
setValidityDelegate,
getValidityDelegate,
(updateInterval) -> {}, // set update interval
() -> 1000, // get update interval
(MAX_ENTRIES) -> {}, // set max entries
() -> 10, // get max entries
(updateActiveUpdate) -> {}, // set active update enabled
() -> false, // get active update enabled
loadFunction,
bulkLoadFunction,
cacheEnabledDelegate);
}
void cleanup()
{
cache.cleanUp();
}
}
}