org.opensearch.common.settings.AbstractScopedSettings Maven / Gradle / Ivy
Show all versions of opensearch Show documentation
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.common.settings;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.lucene.search.spell.LevenshteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.opensearch.ExceptionsHelper;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.regex.Regex;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A basic setting service that can be used for per-index and per-cluster settings.
* This service offers transactional application of updates settings.
*
* @opensearch.internal
*/
public abstract class AbstractScopedSettings {
public static final String ARCHIVED_SETTINGS_PREFIX = "archived.";
private static final Pattern KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])*[-\\w]+$");
private static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+$");
private static final Pattern AFFIX_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+[*](?:[.][-\\w]+)+$");
private final Logger logger;
private final Settings settings;
private final List> settingUpdaters = new CopyOnWriteArrayList<>();
private final Map> complexMatchers;
private final Map> keySettings;
private final Map, SettingUpgrader>> settingUpgraders;
private final Setting.Property scope;
private Settings lastSettingsApplied;
protected AbstractScopedSettings(
final Settings settings,
final Set> settingsSet,
final Set> settingUpgraders,
final Setting.Property scope
) {
this.logger = LogManager.getLogger(this.getClass());
this.settings = settings;
this.lastSettingsApplied = Settings.EMPTY;
this.settingUpgraders = Collections.unmodifiableMap(
settingUpgraders.stream().collect(Collectors.toMap(SettingUpgrader::getSetting, Function.identity()))
);
this.scope = scope;
Map> complexMatchers = new HashMap<>();
Map> keySettings = new HashMap<>();
for (Setting> setting : settingsSet) {
if (setting.getProperties().contains(scope) == false) {
throw new IllegalArgumentException(
"Setting " + setting + " must be a " + scope + " setting but has: " + setting.getProperties()
);
}
validateSettingKey(setting);
if (setting.hasComplexMatcher()) {
Setting> overlappingSetting = findOverlappingSetting(setting, complexMatchers);
if (overlappingSetting != null) {
throw new IllegalArgumentException(
"complex setting key: ["
+ setting.getKey()
+ "] overlaps existing setting key: ["
+ overlappingSetting.getKey()
+ "]"
);
}
complexMatchers.putIfAbsent(setting.getKey(), setting);
} else {
keySettings.putIfAbsent(setting.getKey(), setting);
}
}
this.complexMatchers = complexMatchers;
this.keySettings = keySettings;
}
protected void validateSettingKey(Setting> setting) {
if (isValidKey(setting.getKey()) == false
&& (setting.isGroupSetting() && isValidGroupKey(setting.getKey()) || isValidAffixKey(setting.getKey())) == false
|| setting.getKey().endsWith(".0")) {
throw new IllegalArgumentException("illegal settings key: [" + setting.getKey() + "]");
}
}
protected AbstractScopedSettings(Settings nodeSettings, Settings scopeSettings, AbstractScopedSettings other, Logger logger) {
this.logger = logger;
this.settings = nodeSettings;
this.lastSettingsApplied = scopeSettings;
this.scope = other.scope;
complexMatchers = other.complexMatchers;
keySettings = other.keySettings;
settingUpgraders = Collections.unmodifiableMap(new HashMap<>(other.settingUpgraders));
settingUpdaters.addAll(other.settingUpdaters);
}
public boolean registerSetting(Setting> setting) {
validateSettingKey(setting);
if (setting.hasComplexMatcher()) {
return setting != complexMatchers.putIfAbsent(setting.getKey(), setting);
} else {
return setting != keySettings.putIfAbsent(setting.getKey(), setting);
}
}
public boolean unregisterSetting(Setting> setting) {
if (setting.hasComplexMatcher()) {
return setting != complexMatchers.remove(setting.getKey());
} else {
return setting != keySettings.remove(setting.getKey());
}
}
/**
* Returns true
iff the given key is a valid settings key otherwise false
*/
public static boolean isValidKey(String key) {
return KEY_PATTERN.matcher(key).matches();
}
private static boolean isValidGroupKey(String key) {
return GROUP_KEY_PATTERN.matcher(key).matches();
}
// pkg private for tests
static boolean isValidAffixKey(String key) {
return AFFIX_KEY_PATTERN.matcher(key).matches();
}
public Setting.Property getScope() {
return this.scope;
}
/**
* Validates the given settings by running it through all update listeners without applying it. This
* method will not change any settings but will fail if any of the settings can't be applied.
*/
public synchronized Settings validateUpdate(Settings settings) {
final Settings current = Settings.builder().put(this.settings).put(settings).build();
final Settings previous = Settings.builder().put(this.settings).put(this.lastSettingsApplied).build();
List exceptions = new ArrayList<>();
for (SettingUpdater> settingUpdater : settingUpdaters) {
try {
// ensure running this through the updater / dynamic validator
// don't check if the value has changed we wanna test this anyways
settingUpdater.getValue(current, previous);
} catch (RuntimeException ex) {
exceptions.add(ex);
logger.debug(() -> new ParameterizedMessage("failed to prepareCommit settings for [{}]", settingUpdater), ex);
}
}
// here we are exhaustive and record all settings that failed.
ExceptionsHelper.rethrowAndSuppress(exceptions);
return current;
}
/**
* Applies the given settings to all the settings consumers or to none of them. The settings
* will be merged with the node settings before they are applied while given settings override existing node
* settings.
* @param newSettings the settings to apply
* @return the unmerged applied settings
*/
public synchronized Settings applySettings(Settings newSettings) {
if (lastSettingsApplied != null && newSettings.equals(lastSettingsApplied)) {
// nothing changed in the settings, ignore
return newSettings;
}
final Settings current = Settings.builder().put(this.settings).put(newSettings).build();
final Settings previous = Settings.builder().put(this.settings).put(this.lastSettingsApplied).build();
try {
List applyRunnables = new ArrayList<>();
for (SettingUpdater> settingUpdater : settingUpdaters) {
try {
applyRunnables.add(settingUpdater.updater(current, previous));
} catch (Exception ex) {
logger.warn(() -> new ParameterizedMessage("failed to prepareCommit settings for [{}]", settingUpdater), ex);
throw ex;
}
}
for (Runnable settingUpdater : applyRunnables) {
settingUpdater.run();
}
} catch (Exception ex) {
logger.warn("failed to apply settings", ex);
throw ex;
}
return lastSettingsApplied = newSettings;
}
/**
* Adds a settings consumer with a predicate that is only evaluated at update time.
*
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
*
* @param validator an additional validator that is only applied to updates of this setting.
* This is useful to add additional validation to settings at runtime compared to at startup time.
*/
public synchronized void addSettingsUpdateConsumer(Setting setting, Consumer consumer, Consumer validator) {
if (setting != get(setting.getKey())) {
throw new IllegalArgumentException("Setting is not registered for key [" + setting.getKey() + "]");
}
addSettingsUpdater(setting.newUpdater(consumer, logger, validator));
}
/**
* Adds a settings consumer that is only executed if any setting in the supplied list of settings is changed. In that case all the
* settings are specified in the argument are returned.
*
* Also automatically adds empty consumers for all settings in order to activate logging
*/
public synchronized void addSettingsUpdateConsumer(Consumer consumer, List extends Setting>> settings) {
addSettingsUpdater(Setting.groupedSettingsUpdater(consumer, settings));
}
/**
* Adds a settings consumer that is only executed if any setting in the supplied list of settings is changed. In that case all the
* settings are specified in the argument are returned. The validator is run across all specified settings before the settings are
* applied.
*
* Also automatically adds empty consumers for all settings in order to activate logging
*/
public synchronized void addSettingsUpdateConsumer(
Consumer consumer,
List extends Setting>> settings,
Consumer validator
) {
addSettingsUpdater(Setting.groupedSettingsUpdater(consumer, settings, validator));
}
/**
* Adds a settings consumer for affix settings. Affix settings have a namespace associated to it that needs to be available to the
* consumer in order to be processed correctly.
*/
public synchronized void addAffixUpdateConsumer(
Setting.AffixSetting setting,
BiConsumer consumer,
BiConsumer validator
) {
ensureSettingIsRegistered(setting);
addSettingsUpdater(setting.newAffixUpdater(consumer, logger, validator));
}
/**
* Adds a affix settings consumer that accepts the values for two settings. The consumer is only notified if one or both settings change
* and if the provided validator succeeded.
*
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
*
* This method registers a compound updater that is useful if two settings are depending on each other.
* The consumer is always provided with both values even if only one of the two changes.
*/
public synchronized void addAffixUpdateConsumer(
Setting.AffixSetting settingA,
Setting.AffixSetting settingB,
BiConsumer> consumer,
BiConsumer> validator
) {
// it would be awesome to have a generic way to do that ie. a set of settings that map to an object with a builder
// down the road this would be nice to have!
ensureSettingIsRegistered(settingA);
ensureSettingIsRegistered(settingB);
SettingUpdater