diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImpl.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImpl.java index b47370d92059..911a4ad37078 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImpl.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImpl.java @@ -23,7 +23,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.inject.Inject; @@ -36,6 +35,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationGroupDao; import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao; +import org.apache.cloudstack.utils.cache.LazyCache; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -44,8 +44,6 @@ import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.exception.CloudRuntimeException; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; /** * ConfigDepotImpl implements the ConfigDepot and ConfigDepotAdmin interface. @@ -87,17 +85,15 @@ public class ConfigDepotImpl implements ConfigDepot, ConfigDepotAdmin { List _scopedStorages; Set _configured = Collections.synchronizedSet(new HashSet()); Set newConfigs = Collections.synchronizedSet(new HashSet<>()); - Cache configCache; + LazyCache configCache; private HashMap>> _allKeys = new HashMap>>(1007); HashMap>> _scopeLevelConfigsMap = new HashMap>>(); public ConfigDepotImpl() { - configCache = Caffeine.newBuilder() - .maximumSize(512) - .expireAfterWrite(CONFIG_CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS) - .build(); + configCache = new LazyCache<>(512, + CONFIG_CACHE_EXPIRE_SECONDS, this::getConfigStringValueInternal); ConfigKey.init(this); createEmptyScopeLevelMappings(); } @@ -311,7 +307,7 @@ private String getConfigCacheKey(String key, ConfigKey.Scope scope, Long scopeId @Override public String getConfigStringValue(String key, ConfigKey.Scope scope, Long scopeId) { - return configCache.get(getConfigCacheKey(key, scope, scopeId), this::getConfigStringValueInternal); + return configCache.get(getConfigCacheKey(key, scope, scopeId)); } @Override diff --git a/utils/src/main/java/org/apache/cloudstack/utils/cache/LazyCache.java b/utils/src/main/java/org/apache/cloudstack/utils/cache/LazyCache.java new file mode 100644 index 000000000000..0b4c91e24b3c --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/cache/LazyCache.java @@ -0,0 +1,48 @@ +// 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.cloudstack.utils.cache; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +public class LazyCache { + + private final LoadingCache cache; + + public LazyCache(long maximumSize, long expireAfterWriteSeconds, Function loader) { + this.cache = Caffeine.newBuilder() + .maximumSize(maximumSize) + .expireAfterWrite(expireAfterWriteSeconds, TimeUnit.SECONDS) + .build(loader::apply); + } + + public V get(K key) { + return cache.get(key); + } + + public void invalidate(K key) { + cache.invalidate(key); + } + + public void clear() { + cache.invalidateAll(); + } +} diff --git a/utils/src/test/java/org/apache/cloudstack/utils/cache/LazyCacheTest.java b/utils/src/test/java/org/apache/cloudstack/utils/cache/LazyCacheTest.java new file mode 100644 index 000000000000..75d31b95fcc3 --- /dev/null +++ b/utils/src/test/java/org/apache/cloudstack/utils/cache/LazyCacheTest.java @@ -0,0 +1,115 @@ +// 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.cloudstack.utils.cache; + +import static org.junit.Assert.assertEquals; + +import java.util.function.Function; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LazyCacheTest { + private final long expireSeconds = 1; + private final String cacheValuePrefix = "ComputedValueFor:"; + private LazyCache cache; + private Function mockLoader; + + @Before + public void setUp() { + mockLoader = Mockito.mock(Function.class); + Mockito.when(mockLoader.apply(Mockito.anyString())).thenAnswer(invocation -> cacheValuePrefix + invocation.getArgument(0)); + cache = new LazyCache<>(4, expireSeconds, mockLoader); + } + + @Test + public void testCacheMissAndLoader() { + String key = "key1"; + String value = cache.get(key); + assertEquals(cacheValuePrefix + key, value); + Mockito.verify(mockLoader).apply(key); + } + + @Test + public void testLoaderNotCalledIfPresent() { + String key = "key2"; + cache.get(key); + try { + Thread.sleep((long)(0.9 * expireSeconds * 1000)); + } catch (InterruptedException ie) { + Assert.fail(String.format("Exception occurred: %s", ie.getMessage())); + } + cache.get(key); + Mockito.verify(mockLoader, Mockito.times(1)).apply(key); + } + + @Test + public void testCacheExpiration() { + String key = "key3"; + cache.get(key); + try { + Thread.sleep((long)(1.1 * expireSeconds * 1000)); + } catch (InterruptedException ie) { + Assert.fail(String.format("Exception occurred: %s", ie.getMessage())); + } + cache.get(key); + Mockito.verify(mockLoader, Mockito.times(2)).apply(key); + } + + @Test + public void testInvalidateKey() { + String key = "key4"; + cache.get(key); + cache.invalidate(key); + cache.get(key); + Mockito.verify(mockLoader, Mockito.times(2)).apply(key); + } + + @Test + public void testClearCache() { + String key1 = "key5"; + String key2 = "key6"; + cache.get(key1); + cache.get(key2); + cache.clear(); + cache.get(key1); + Mockito.verify(mockLoader, Mockito.times(2)).apply(key1); + Mockito.verify(mockLoader, Mockito.times(1)).apply(key2); + } + + @Test + public void testMaximumSize() { + String key = "key7"; + cache.get(key); + for (int i = 0; i < 4; i++) { + cache.get(String.format("newkey-%d", i)); + } + try { + Thread.sleep(100); + } catch (InterruptedException ie) { + Assert.fail(String.format("Exception occurred: %s", ie.getMessage())); + } + cache.get(key); + Mockito.verify(mockLoader, Mockito.times(2)).apply(key); + } +}