Skip to content

Commit

Permalink
utils: add wrapper for the loading cache
Browse files Browse the repository at this point in the history
Follow up for apache#9638
Creates a utility class LazyCache which currently wraps Caffeine library Cache class.

Signed-off-by: Abhishek Kumar <[email protected]>
  • Loading branch information
shwstppr committed Sep 5, 2024
1 parent 2245d98 commit 3842f13
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -87,17 +85,15 @@ public class ConfigDepotImpl implements ConfigDepot, ConfigDepotAdmin {
List<ScopedConfigStorage> _scopedStorages;
Set<Configurable> _configured = Collections.synchronizedSet(new HashSet<Configurable>());
Set<String> newConfigs = Collections.synchronizedSet(new HashSet<>());
Cache<String, String> configCache;
LazyCache<String, String> configCache;

private HashMap<String, Pair<String, ConfigKey<?>>> _allKeys = new HashMap<String, Pair<String, ConfigKey<?>>>(1007);

HashMap<ConfigKey.Scope, Set<ConfigKey<?>>> _scopeLevelConfigsMap = new HashMap<ConfigKey.Scope, Set<ConfigKey<?>>>();

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();
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<K, V> {

private final LoadingCache<K, V> cache;

public LazyCache(long maximumSize, long expireAfterWriteSeconds, Function<K, V> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> cache;
private Function<String, String> 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);
}
}

0 comments on commit 3842f13

Please sign in to comment.