Skip to content

Commit 7b0b037

Browse files
mprokopchuksb-abhish3k
authored andcommitted
Configuration keys performance update
Configuration keys: cache inherited parent-scope value on scope miss to avoid repeated DB lookups. Implemented caching inherited parent-scope value under "current" scope to avoid repeated configuration key hierarchy traversal.
1 parent 10037c8 commit 7b0b037

11 files changed

Lines changed: 346 additions & 30 deletions

File tree

framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,15 @@ public T valueInScope(Scope scope, Long id) {
434434
}
435435
String value = s_depot != null ? s_depot.getConfigStringValue(_name, scope, id) : null;
436436
if (value == null) {
437-
return valueInGlobalOrAvailableParentScope(scope, id);
437+
T parentValue = valueInGlobalOrAvailableParentScope(scope, id);
438+
// Cache the inherited value under this scope to avoid repeated hierarchy traversal.
439+
// Skip this for keys with a multiplier: parentValue already has the multiplier applied,
440+
// so caching its toString() and reading it back through valueOf() would apply the
441+
// multiplier a second time (double scaling).
442+
if (s_depot != null && parentValue != null && multiplier() == null) {
443+
s_depot.cacheValue(_name, scope, id, parentValue.toString());
444+
}
445+
return parentValue;
438446
}
439447
logger.trace("Scope({}) value for config ({}): {}", scope, _name, _value);
440448

framework/config/src/main/java/org/apache/cloudstack/framework/config/dao/ConfigurationDao.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,6 @@ public interface ConfigurationDao extends GenericDao<ConfigurationVO, String> {
7070
void invalidateCache();
7171

7272
List<ConfigurationVO> searchPartialConfigurations();
73+
74+
String getValueByKey(String key);
7375
}

framework/config/src/main/java/org/apache/cloudstack/framework/config/dao/ConfigurationDaoImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,10 @@ public List<ConfigurationVO> searchPartialConfigurations() {
218218
SearchCriteria<ConfigurationVO> sc = PartialSearch.create();
219219
return searchIncludingRemoved(sc, null, null, false);
220220
}
221+
222+
@Override
223+
public String getValueByKey(String key) {
224+
ConfigurationVO configVO = findByName(key);
225+
return (configVO == null ? null : configVO.getValue());
226+
}
221227
}

framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImpl.java

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,8 @@
2424
import java.util.List;
2525
import java.util.Set;
2626

27-
import javax.annotation.PostConstruct;
2827
import javax.inject.Inject;
2928

30-
import com.cloud.utils.db.Transaction;
31-
import com.cloud.utils.db.TransactionCallback;
3229
import org.apache.cloudstack.framework.config.ConfigDepot;
3330
import org.apache.cloudstack.framework.config.ConfigDepotAdmin;
3431
import org.apache.cloudstack.framework.config.ConfigKey;
@@ -38,13 +35,16 @@
3835
import org.apache.cloudstack.framework.config.dao.ConfigurationGroupDao;
3936
import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao;
4037
import org.apache.cloudstack.utils.cache.LazyCache;
38+
import org.apache.commons.beanutils.ConvertUtils;
4139
import org.apache.commons.lang.ObjectUtils;
4240
import org.apache.commons.lang3.StringUtils;
4341
import org.apache.logging.log4j.LogManager;
4442
import org.apache.logging.log4j.Logger;
4543

4644
import com.cloud.utils.Pair;
4745
import com.cloud.utils.Ternary;
46+
import com.cloud.utils.db.Transaction;
47+
import com.cloud.utils.db.TransactionCallback;
4848
import com.cloud.utils.exception.CloudRuntimeException;
4949

5050
/**
@@ -74,9 +74,18 @@
7474
* when constructing a ConfigKey then configuration server should use the
7575
* validation class to validate the value the admin input for the key.
7676
*/
77-
public class ConfigDepotImpl implements ConfigDepot, ConfigDepotAdmin {
77+
public class ConfigDepotImpl implements ConfigDepot, ConfigDepotAdmin, Configurable {
7878
protected Logger logger = LogManager.getLogger(getClass());
79+
7980
protected final static long CONFIG_CACHE_EXPIRE_SECONDS = 30;
81+
82+
protected final ConfigKey<Long> ConfigKeyCacheMaxSize = new ConfigKey<>("Advanced", Long.class, "config.key.cache.max.size", "512",
83+
"Configuration keys cache max size", false);
84+
protected final ConfigKey<Long> ConfigKeyCacheRefreshIntervalSeconds = new ConfigKey<>("Advanced", Long.class, "config.key.expire.seconds", String.valueOf(CONFIG_CACHE_EXPIRE_SECONDS),
85+
"Configuration keys cache refresh interval in seconds", false);
86+
protected final ConfigKey<Boolean> ConfigKeyCacheRefreshAfterWrite = new ConfigKey<>("Advanced", Boolean.class, "config.key.cache.refresh.after.write", "false",
87+
"When true the configuration cache refreshes entries asynchronously and serves the stale value during reload (non-blocking); when false entries expire and the next read blocks to load a fresh value (stronger consistency across management servers)", false);
88+
8089
@Inject
8190
ConfigurationDao _configDao;
8291
@Inject
@@ -87,15 +96,13 @@ public class ConfigDepotImpl implements ConfigDepot, ConfigDepotAdmin {
8796
List<ScopedConfigStorage> _scopedStorages;
8897
Set<Configurable> _configured = Collections.synchronizedSet(new HashSet<Configurable>());
8998
Set<String> newConfigs = Collections.synchronizedSet(new HashSet<>());
90-
LazyCache<Ternary<String, ConfigKey.Scope, Long>, String> configCache;
99+
volatile LazyCache<Ternary<String, ConfigKey.Scope, Long>, String> configCache;
91100

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

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

96105
public ConfigDepotImpl() {
97-
configCache = new LazyCache<>(512,
98-
CONFIG_CACHE_EXPIRE_SECONDS, this::getConfigStringValueInternal);
99106
ConfigKey.init(this);
100107
createEmptyScopeLevelMappings();
101108
}
@@ -121,7 +128,63 @@ public ConfigKey<?> get(String key) {
121128
return value != null ? value.second() : null;
122129
}
123130

124-
@PostConstruct
131+
@SuppressWarnings("unchecked")
132+
private <T> T getConfigValue(ConfigKey<T> configKey) {
133+
String valueString;
134+
try {
135+
valueString = _configDao.getValueByKey(configKey.key());
136+
if (valueString == null) {
137+
valueString = configKey.defaultValue();
138+
}
139+
} catch (CloudRuntimeException e) {
140+
String msg = "Failed to retrieve configuration value for: " + configKey.key();
141+
logger.error(msg, e);
142+
throw e;
143+
} catch (Exception e) {
144+
String msg = "Failed to retrieve configuration value for: " + configKey.key();
145+
logger.error(msg, e);
146+
throw new CloudRuntimeException(msg, e);
147+
}
148+
return (T) ConvertUtils.convert(valueString, configKey.type());
149+
}
150+
151+
/**
152+
* Lazily initialize the config cache on first access. Reading the cache size and
153+
* TTL here is safe because by the time any caller exercises the cache, Spring's
154+
* refresh() has completed and DatabaseUpgradeChecker has run any pending schema
155+
* migrations, so the configuration table is in its expected shape.
156+
*
157+
* populateConfiguration(this) is invoked here to guarantee that this bean's own
158+
* ConfigKeys end up registered in _allKeys and persisted to the configuration
159+
* table even when Spring's List<Configurable> autowiring excludes self. The call
160+
* is idempotent: if ConfigurationServerImpl.populateConfigurations() already
161+
* iterated over this bean, the _configured guard inside populateConfiguration
162+
* makes it a no-op.
163+
*
164+
* The cache-tuning keys are read directly from the configuration table via
165+
* getConfigValue(); they fall back to their defaults when no row is present. They are
166+
* applied only here at initialization (the cache is built once), so changing them
167+
* requires a restart.
168+
*/
169+
private void ensureCacheInitialized() {
170+
if (configCache == null) {
171+
synchronized (this) {
172+
if (configCache == null) {
173+
populateConfiguration(this);
174+
Long maxSize = getConfigValue(ConfigKeyCacheMaxSize);
175+
Long expirationSeconds = getConfigValue(ConfigKeyCacheRefreshIntervalSeconds);
176+
Boolean refreshAfterWrite = getConfigValue(ConfigKeyCacheRefreshAfterWrite);
177+
if (logger.isDebugEnabled()) {
178+
logger.debug("{} value: {}", ConfigKeyCacheMaxSize.key(), maxSize);
179+
logger.debug("{} value: {}", ConfigKeyCacheRefreshIntervalSeconds.key(), expirationSeconds);
180+
logger.debug("{} value: {}", ConfigKeyCacheRefreshAfterWrite.key(), refreshAfterWrite);
181+
}
182+
configCache = new LazyCache<>(maxSize, expirationSeconds, refreshAfterWrite, this::getConfigStringValueInternal);
183+
}
184+
}
185+
}
186+
}
187+
125188
@Override
126189
public void populateConfigurations() {
127190
Date date = new Date();
@@ -282,6 +345,7 @@ protected String getConfigStringValueInternal(Ternary<String, ConfigKey.Scope, L
282345
final String key = cacheKey.first();
283346
final ConfigKey.Scope scope = cacheKey.second();
284347
final Long scopeId = cacheKey.third();
348+
logger.debug("Fetching config key from DB: key={}, scope={}, scopeId={}", key, scope, scopeId);
285349
if (!ConfigKey.Scope.Global.equals(scope) && scopeId != null) {
286350
ScopedConfigStorage scopedConfigStorage = getScopedStorage(scope);
287351
if (scopedConfigStorage == null) {
@@ -290,11 +354,7 @@ protected String getConfigStringValueInternal(Ternary<String, ConfigKey.Scope, L
290354
final ScopedConfigStorage scopedConfigStorageFinal = scopedConfigStorage;
291355
return Transaction.execute((TransactionCallback<String>) status -> scopedConfigStorageFinal.getConfigValue(scopeId, key));
292356
}
293-
ConfigurationVO configurationVO = _configDao.findById(key);
294-
if (configurationVO != null) {
295-
return configurationVO.getValue();
296-
}
297-
return null;
357+
return _configDao.getValueByKey(key);
298358
}
299359

300360
protected Ternary<String, ConfigKey.Scope, Long> getConfigCacheKey(String key, ConfigKey.Scope scope, Long scopeId) {
@@ -303,11 +363,22 @@ protected Ternary<String, ConfigKey.Scope, Long> getConfigCacheKey(String key, C
303363

304364
@Override
305365
public String getConfigStringValue(String key, ConfigKey.Scope scope, Long scopeId) {
366+
ensureCacheInitialized();
306367
return configCache.get(getConfigCacheKey(key, scope, scopeId));
307368
}
308369

370+
/**
371+
* Inserts a value directly into the config cache without persisting to DB.
372+
* Used to cache inherited values (e.g. from a parent scope) under a more specific scope key.
373+
*/
374+
public void cacheValue(String key, ConfigKey.Scope scope, Long scopeId, String value) {
375+
ensureCacheInitialized();
376+
configCache.put(getConfigCacheKey(key, scope, scopeId), value);
377+
}
378+
309379
@Override
310380
public void invalidateConfigCache(String key, ConfigKey.Scope scope, Long scopeId) {
381+
ensureCacheInitialized();
311382
configCache.invalidate(getConfigCacheKey(key, scope, scopeId));
312383
}
313384

@@ -397,4 +468,14 @@ public Pair<ConfigKey.Scope, Long> getParentScope(ConfigKey.Scope scope, Long id
397468
}
398469
return scopedConfigStorage.getParentScope(id);
399470
}
471+
472+
@Override
473+
public String getConfigComponentName() {
474+
return ConfigDepotImpl.class.getSimpleName();
475+
}
476+
477+
@Override
478+
public ConfigKey<?>[] getConfigKeys() {
479+
return new ConfigKey[]{ConfigKeyCacheMaxSize, ConfigKeyCacheRefreshIntervalSeconds, ConfigKeyCacheRefreshAfterWrite};
480+
}
400481
}

framework/config/src/test/java/org/apache/cloudstack/framework/config/impl/ConfigDepotAdminTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ public void setUp() throws Exception {
8888
_depotAdmin._configDao = _configDao;
8989
_depotAdmin._configGroupDao = _configGroupDao;
9090
_depotAdmin._configSubGroupDao = _configSubGroupDao;
91-
_depotAdmin._configurables = new ArrayList<Configurable>();
91+
_depotAdmin._configurables = new ArrayList<>();
9292
_depotAdmin._configurables.add(_configurable);
93-
_depotAdmin._scopedStorages = new ArrayList<ScopedConfigStorage>();
93+
_depotAdmin._scopedStorages = new ArrayList<>();
9494
_depotAdmin._scopedStorages.add(_scopedStorage);
9595
}
9696

framework/config/src/test/java/org/apache/cloudstack/framework/config/impl/ConfigDepotImplTest.java

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
3131
import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao;
3232
import org.junit.Assert;
33+
import org.junit.Before;
3334
import org.junit.Test;
3435
import org.junit.runner.RunWith;
3536
import org.mockito.ArgumentCaptor;
@@ -41,6 +42,7 @@
4142

4243
import com.cloud.utils.Pair;
4344

45+
4446
@RunWith(MockitoJUnitRunner.class)
4547
public class ConfigDepotImplTest {
4648

@@ -55,6 +57,12 @@ public class ConfigDepotImplTest {
5557
@InjectMocks
5658
private ConfigDepotImpl configDepotImpl = new ConfigDepotImpl();
5759

60+
@Before
61+
public void setUp() {
62+
configDepotImpl.setConfigurables(Collections.emptyList());
63+
configDepotImpl.populateConfigurations();
64+
}
65+
5866
@Test
5967
public void createConfigObjectPersistsSubGroupWithNameAndGroupId() {
6068
ConfigKey<?> key = Mockito.mock(ConfigKey.class);
@@ -102,9 +110,7 @@ public void testIsNewConfig() {
102110
}
103111

104112
private void runTestGetConfigStringValue(String key, String value) {
105-
ConfigurationVO configurationVO = Mockito.mock(ConfigurationVO.class);
106-
Mockito.when(configurationVO.getValue()).thenReturn(value);
107-
Mockito.when(_configDao.findById(key)).thenReturn(configurationVO);
113+
Mockito.when(_configDao.getValueByKey(key)).thenReturn(value);
108114
String result = configDepotImpl.getConfigStringValue(key, ConfigKey.Scope.Global, null);
109115
Assert.assertEquals(value, result);
110116
}
@@ -131,7 +137,7 @@ private void runTestGetConfigStringValueExpiry(long wait, int configDBRetrieval)
131137
}
132138
String result = configDepotImpl.getConfigStringValue(key, ConfigKey.Scope.Global, null);
133139
Assert.assertEquals(value, result);
134-
Mockito.verify(_configDao, Mockito.times(configDBRetrieval)).findById(key);
140+
Mockito.verify(_configDao, Mockito.timeout(2000).times(configDBRetrieval)).getValueByKey(key);
135141
}
136142

137143
@Test
@@ -199,6 +205,58 @@ public ConfigKey<?>[] getConfigKeys() {
199205
Mockito.verify(_configDao, Mockito.times(1)).persist(configurationVO);
200206
}
201207

208+
@Test
209+
public void testParentScopeValueCachedUnderChildScopeOnMiss() {
210+
String keyName = "test.key";
211+
ConfigKey<String> key = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, String.class,
212+
keyName, "default-value", "test", true, List.of(ConfigKey.Scope.Cluster, ConfigKey.Scope.Zone));
213+
214+
Long clusterId = 1L;
215+
Long zoneId = 2L;
216+
String zoneValue = "zone-value";
217+
218+
ScopedConfigStorage clusterStorage = Mockito.mock(ScopedConfigStorage.class);
219+
Mockito.when(clusterStorage.getScope()).thenReturn(ConfigKey.Scope.Cluster);
220+
Mockito.when(clusterStorage.getConfigValue(clusterId, keyName)).thenReturn(null);
221+
Mockito.when(clusterStorage.getParentScope(clusterId)).thenReturn(new Pair<>(ConfigKey.Scope.Zone, zoneId));
222+
223+
ScopedConfigStorage zoneStorage = Mockito.mock(ScopedConfigStorage.class);
224+
Mockito.when(zoneStorage.getScope()).thenReturn(ConfigKey.Scope.Zone);
225+
Mockito.when(zoneStorage.getConfigValue(zoneId, keyName)).thenReturn(zoneValue);
226+
227+
configDepotImpl.setScopedStorages(List.of(clusterStorage, zoneStorage));
228+
229+
// first call: no cluster value, traverses to zone, zone value is cached under cluster scope key
230+
Assert.assertEquals(zoneValue, key.valueInScope(ConfigKey.Scope.Cluster, clusterId));
231+
232+
// second call: cache hit with zone value, cluster storage not queried again
233+
Assert.assertEquals(zoneValue, key.valueInScope(ConfigKey.Scope.Cluster, clusterId));
234+
Mockito.verify(clusterStorage, Mockito.times(1)).getConfigValue(clusterId, keyName);
235+
}
236+
237+
@Test
238+
public void testDefaultValueCachedUnderChildScopeOnMiss() {
239+
String keyName = "test.key";
240+
String keyValue = "default-value";
241+
ConfigKey<String> key = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, String.class,
242+
keyName, keyValue, "test", true, ConfigKey.Scope.Cluster);
243+
244+
Long clusterId = 1L;
245+
246+
ScopedConfigStorage clusterStorage = Mockito.mock(ScopedConfigStorage.class);
247+
Mockito.when(clusterStorage.getScope()).thenReturn(ConfigKey.Scope.Cluster);
248+
Mockito.when(clusterStorage.getConfigValue(clusterId, keyName)).thenReturn(null);
249+
250+
configDepotImpl.setScopedStorages(List.of(clusterStorage));
251+
252+
// first call: no cluster value, traverses to default, default value is cached under cluster scope key
253+
Assert.assertEquals(keyValue, key.valueInScope(ConfigKey.Scope.Cluster, clusterId));
254+
255+
// second call: cache hit with default value, cluster storage not queried again
256+
Assert.assertEquals(keyValue, key.valueInScope(ConfigKey.Scope.Cluster, clusterId));
257+
Mockito.verify(clusterStorage, Mockito.times(1)).getConfigValue(clusterId, keyName);
258+
}
259+
202260
@Test
203261
public void getParentScopeWithValidScope() {
204262
ConfigKey.Scope scope = ConfigKey.Scope.Cluster;
@@ -217,4 +275,36 @@ public void getParentScopeWithValidScope() {
217275
Assert.assertEquals(parentScope, result.first());
218276
Assert.assertEquals(parentId, result.second());
219277
}
278+
279+
@Test
280+
public void testCacheNotInitializedBeforeFirstAccess() {
281+
Assert.assertNull("configCache should be null until first access triggers lazy init",
282+
configDepotImpl.configCache);
283+
}
284+
285+
@Test
286+
public void testCacheInitializedOnFirstAccess() {
287+
Assert.assertNull(configDepotImpl.configCache);
288+
configDepotImpl.getConfigStringValue("anyKey", ConfigKey.Scope.Global, null);
289+
Assert.assertNotNull("configCache should be initialized after first access",
290+
configDepotImpl.configCache);
291+
}
292+
293+
@Test
294+
public void testCacheInitializedOnlyOnce() {
295+
configDepotImpl.getConfigStringValue("key1", ConfigKey.Scope.Global, null);
296+
configDepotImpl.getConfigStringValue("key2", ConfigKey.Scope.Global, null);
297+
configDepotImpl.getConfigStringValue("key3", ConfigKey.Scope.Global, null);
298+
Mockito.verify(_configDao, Mockito.times(1)).getValueByKey("config.key.cache.max.size");
299+
Mockito.verify(_configDao, Mockito.times(1)).getValueByKey("config.key.expire.seconds");
300+
}
301+
302+
@Test
303+
public void testCacheUsesDefaultsWhenConfigKeysAbsentInDB() {
304+
Mockito.when(_configDao.getValueByKey("config.key.cache.max.size")).thenReturn(null);
305+
Mockito.when(_configDao.getValueByKey("config.key.expire.seconds")).thenReturn(null);
306+
configDepotImpl.getConfigStringValue("anyKey", ConfigKey.Scope.Global, null);
307+
Assert.assertNotNull("Cache should initialize successfully with defaults when DB returns null",
308+
configDepotImpl.configCache);
309+
}
220310
}

server/src/test/java/com/cloud/vpc/dao/MockConfigurationDaoImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,9 @@ public void invalidateCache() {
123123
public List<ConfigurationVO> searchPartialConfigurations() {
124124
return List.of();
125125
}
126+
127+
@Override
128+
public String getValueByKey(String key) {
129+
return null;
130+
}
126131
}

0 commit comments

Comments
 (0)