2424import java .util .List ;
2525import java .util .Set ;
2626
27- import javax .annotation .PostConstruct ;
2827import javax .inject .Inject ;
2928
30- import com .cloud .utils .db .Transaction ;
31- import com .cloud .utils .db .TransactionCallback ;
3229import org .apache .cloudstack .framework .config .ConfigDepot ;
3330import org .apache .cloudstack .framework .config .ConfigDepotAdmin ;
3431import org .apache .cloudstack .framework .config .ConfigKey ;
3835import org .apache .cloudstack .framework .config .dao .ConfigurationGroupDao ;
3936import org .apache .cloudstack .framework .config .dao .ConfigurationSubGroupDao ;
4037import org .apache .cloudstack .utils .cache .LazyCache ;
38+ import org .apache .commons .beanutils .ConvertUtils ;
4139import org .apache .commons .lang .ObjectUtils ;
4240import org .apache .commons .lang3 .StringUtils ;
4341import org .apache .logging .log4j .LogManager ;
4442import org .apache .logging .log4j .Logger ;
4543
4644import com .cloud .utils .Pair ;
4745import com .cloud .utils .Ternary ;
46+ import com .cloud .utils .db .Transaction ;
47+ import com .cloud .utils .db .TransactionCallback ;
4848import com .cloud .utils .exception .CloudRuntimeException ;
4949
5050/**
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}
0 commit comments