/* * Copyright 2017 ThoughtWorks, Inc. * * Licensed 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 com.thoughtworks.go.config; import com.thoughtworks.go.config.commands.EntityConfigUpdateCommand; import com.thoughtworks.go.config.update.FullConfigUpdateCommand; import com.thoughtworks.go.config.validation.GoConfigValidity; import com.thoughtworks.go.domain.ConfigErrors; import com.thoughtworks.go.listener.ConfigChangedListener; import com.thoughtworks.go.listener.EntityConfigChangedListener; import com.thoughtworks.go.server.domain.Username; import com.thoughtworks.go.serverhealth.HealthStateType; import com.thoughtworks.go.serverhealth.ServerHealthService; import com.thoughtworks.go.serverhealth.ServerHealthState; import com.thoughtworks.go.util.SystemEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import static com.thoughtworks.go.server.service.GoConfigService.INVALID_CRUISE_CONFIG_XML; import static com.thoughtworks.go.util.ExceptionUtils.bomb; /** * @understands when to reload the config file or other config source */ @Component public class CachedGoConfig { private static final Logger LOGGER = LoggerFactory.getLogger(CachedGoConfig.class); private final GoFileConfigDataSource dataSource; private final CachedGoPartials cachedGoPartials; private GoConfigMigrator goConfigMigrator; private SystemEnvironment systemEnvironment; private final ServerHealthService serverHealthService; private List<ConfigChangedListener> listeners = new ArrayList<>(); private volatile CruiseConfig currentConfig; private volatile CruiseConfig currentConfigForEdit; private volatile CruiseConfig mergedCurrentConfigForEdit; private volatile GoConfigHolder configHolder; private volatile Exception lastException; @Autowired public CachedGoConfig(ServerHealthService serverHealthService, GoFileConfigDataSource dataSource, CachedGoPartials cachedGoPartials, GoConfigMigrator goConfigMigrator, SystemEnvironment systemEnvironment) { this.serverHealthService = serverHealthService; this.dataSource = dataSource; this.cachedGoPartials = cachedGoPartials; this.goConfigMigrator = goConfigMigrator; this.systemEnvironment = systemEnvironment; } public static List<ConfigErrors> validate(CruiseConfig config) { List<ConfigErrors> validationErrors = new ArrayList<>(); validationErrors.addAll(config.validateAfterPreprocess()); return validationErrors; } //used in tests public void throwExceptionIfExists() { if (lastException != null) { throw bomb("Invalid config file", lastException); } } //NOTE: This method is called on a thread from Spring public void onTimer() { this.forceReload(); } public void forceReload() { LOGGER.debug("Config file (on disk) update check is in queue"); synchronized (GoConfigWriteLock.class) { LOGGER.debug("Config file (on disk) update check is in progress"); try { GoConfigHolder configHolder = dataSource.load(); if (configHolder != null) { saveValidConfigToCacheAndNotifyConfigChangeListeners(configHolder); } } catch (Exception e) { LOGGER.warn("Error loading cruise-config.xml from disk, keeping previous one", e); saveConfigError(e); } } } private synchronized void saveConfigError(Exception e) { this.lastException = e; ServerHealthState state = ServerHealthState.error(INVALID_CRUISE_CONFIG_XML, GoConfigValidity.invalid(e).errorMessage(), HealthStateType.invalidConfig()); serverHealthService.update(state); } public CruiseConfig loadForEditing() { loadConfigIfNull(); return currentConfigForEdit; } public CruiseConfig loadMergedForEditing() { loadConfigIfNull(); if(mergedCurrentConfigForEdit == null) { // when there are no partials, just return standard config for edit return currentConfigForEdit; } return mergedCurrentConfigForEdit; } public CruiseConfig currentConfig() { if (currentConfig == null) { currentConfig = new BasicCruiseConfig(); } return currentConfig; } public void loadConfigIfNull() { if (currentConfig == null || currentConfigForEdit == null || configHolder == null || (mergedCurrentConfigForEdit == null && !cachedGoPartials.lastValidPartials().isEmpty())) { forceReload(); } } public synchronized ConfigSaveState writeFullConfigWithLock(FullConfigUpdateCommand updateConfigCommand) { GoFileConfigDataSource.GoConfigSaveResult saveResult = dataSource.writeFullConfigWithLock(updateConfigCommand, this.configHolder); saveValidConfigToCacheAndNotifyConfigChangeListeners(saveResult.getConfigHolder()); return saveResult.getConfigSaveState(); } public synchronized void upgradeConfig() throws Exception { if(systemEnvironment.optimizeFullConfigSave()) { GoConfigHolder goConfigHolder = goConfigMigrator.migrate(); saveValidConfigToCacheAndNotifyConfigChangeListeners(goConfigHolder); } else { dataSource.upgradeIfNecessary(); } } public synchronized ConfigSaveState writeWithLock(UpdateConfigCommand updateConfigCommand) { GoFileConfigDataSource.GoConfigSaveResult saveResult = dataSource.writeWithLock(updateConfigCommand, this.configHolder); saveValidConfigToCacheAndNotifyConfigChangeListeners(saveResult.getConfigHolder()); return saveResult.getConfigSaveState(); } public synchronized EntityConfigSaveResult writeEntityWithLock(EntityConfigUpdateCommand updateConfigCommand, Username currentUser) { EntityConfigSaveResult entityConfigSaveResult = dataSource.writeEntityWithLock(updateConfigCommand, this.configHolder, currentUser); saveValidConfigToCacheAndNotifyEntityConfigChangeListeners(entityConfigSaveResult); return entityConfigSaveResult; } private <T> void saveValidConfigToCacheAndNotifyEntityConfigChangeListeners(EntityConfigSaveResult<T> saveResult) { saveValidConfigToCache(saveResult.getConfigHolder()); LOGGER.info("About to notify {} config listeners", saveResult.getEntityConfig().getClass().getName()); for (ConfigChangedListener listener : listeners) { if(listener instanceof EntityConfigChangedListener<?> && ((EntityConfigChangedListener) listener).shouldCareAbout(saveResult.getEntityConfig())){ try { long startTime = System.currentTimeMillis(); @SuppressWarnings("unchecked") EntityConfigChangedListener<T> entityConfigChangedListener = (EntityConfigChangedListener<T>) listener; entityConfigChangedListener.onEntityConfigChange(saveResult.getEntityConfig()); LOGGER.debug("Notifying {} took (in ms): {}", listener.getClass(), (System.currentTimeMillis() - startTime)); } catch (Exception e) { LOGGER.error("failed to fire config changed event for listener: " + listener, e); } } } LOGGER.info("Finished notifying {} config listeners", saveResult.getEntityConfig().getClass().getName()); } private synchronized void saveValidConfigToCache(GoConfigHolder configHolder) { if (configHolder != null) { LOGGER.debug("[Config Save] Saving config to the cache"); this.lastException = null; this.configHolder = configHolder; this.currentConfig = this.configHolder.config; this.currentConfigForEdit = this.configHolder.configForEdit; this.mergedCurrentConfigForEdit = configHolder.mergedConfigForEdit; serverHealthService.update(ServerHealthState.success(HealthStateType.invalidConfig())); } } private synchronized void saveValidConfigToCacheAndNotifyConfigChangeListeners(GoConfigHolder configHolder) { saveValidConfigToCache(configHolder); if (configHolder != null) { notifyListeners(currentConfig); } } public String getFileLocation() { return dataSource.getFileLocation(); } public synchronized void save(String configFileContent, boolean shouldMigrate) throws Exception { GoConfigHolder newConfigHolder = dataSource.write(configFileContent, shouldMigrate); saveValidConfigToCacheAndNotifyConfigChangeListeners(newConfigHolder); } public GoConfigValidity checkConfigFileValid() { Exception ex = lastException; if (ex != null) { return GoConfigValidity.invalid(ex); } return GoConfigValidity.valid(); } public synchronized void registerListener(ConfigChangedListener listener) { this.listeners.add(listener); if (currentConfig != null) { listener.onConfigChange(currentConfig); } } private synchronized void notifyListeners(CruiseConfig newCruiseConfig) { LOGGER.info("About to notify config listeners"); for (ConfigChangedListener listener : listeners) { try { listener.onConfigChange(newCruiseConfig); } catch (Exception e) { LOGGER.error("Failed to fire config changed event for listener: " + listener, e); } } LOGGER.info("Finished notifying all listeners"); } /** * @deprecated Used only in tests */ public synchronized void clearListeners() { listeners.clear(); } /** * @deprecated Used only in tests */ public void reloadListeners() { notifyListeners(currentConfig()); } public GoConfigHolder loadConfigHolder() { return configHolder; } public boolean hasListener(ConfigChangedListener listener) { return this.listeners.contains(listener); } }