From 7f16a1d9a73525b77c6816ec913c9b2ba86d79a7 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Mon, 21 Mar 2016 13:02:15 +0100 Subject: [PATCH] Improve upgrade experience of node level index settings In 5.0 we don't allow index settings to be specified on the node level ie. in yaml files or via commandline argument. This can cause problems during upgrade if this was used extensively. For instance if analyzers where specified on a node level this might cause the index to be closed when imported (see #17187). In such a case all indices relying on this must be updated via `PUT /${index}/_settings`. Yet, this API has slightly different semantics since it overrides existing settings. To make this less painful this change adds a `preserve_existing` parameter on that API to ensure we have the same semantics as if the setting was applied on the node level. This change also adds a better error message and a change to the migration guide to ensure upgrades are smooth if index settings are specified on the node level. If a index setting is detected this change fails the node startup and prints a message like this: ``` ************************************************************************************* Found index level settings on node level configuration. Since elasticsearch 5.x index level settings can NOT be set on the nodes configuration like the elasticsearch.yaml, in system properties or command line arguments.In order to upgrade all indices the settings must be updated via the /${index}/_settings API. Unless all settings are dynamic all indices must be closed in order to apply the upgradeIndices created in the future should use index templates to set default values. Please ensure all required values are updated on all indices by executing: curl -XPUT 'http://localhost:9200/_all/_settings?preserve_existing=true' -d '{ "index.number_of_shards" : "1", "index.query.default_field" : "main_field", "index.translog.durability" : "async", "index.ttl.disable_purge" : "true" }' ************************************************************************************* ``` --- .../put/TransportUpdateSettingsAction.java | 1 + ...dateSettingsClusterStateUpdateRequest.java | 17 ++++- .../settings/put/UpdateSettingsRequest.java | 20 ++++++ .../put/UpdateSettingsRequestBuilder.java | 5 ++ .../MetaDataUpdateSettingsService.java | 9 ++- .../settings/AbstractScopedSettings.java | 15 ++++- .../common/settings/SettingsModule.java | 64 +++++++++++++++++- .../settings/RestUpdateSettingsAction.java | 2 + .../common/settings/SettingsModuleTests.java | 35 ++++++++-- .../migration/migrate_5_0/settings.asciidoc | 9 +++ .../api/indices.put_settings.json | 4 ++ .../test/indices.put_settings/10_basic.yaml | 67 +++++++++++++++++++ 12 files changed, 237 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/TransportUpdateSettingsAction.java b/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/TransportUpdateSettingsAction.java index 36fa1895af4..b20f636977a 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/TransportUpdateSettingsAction.java +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/TransportUpdateSettingsAction.java @@ -80,6 +80,7 @@ public class TransportUpdateSettingsAction extends TransportMasterNodeActiontrue iff the settings update should only add but not update settings. If the setting already exists + * it should not be overwritten by this update. The default is false + */ + public boolean isPreserveExisting() { + return preserveExisting; + } + + /** + * Iff set to true this settings update will only add settings not already set on an index. Existing settings remain + * unchanged. + */ + public UpdateSettingsClusterStateUpdateRequest setPreserveExisting(boolean preserveExisting) { + this.preserveExisting = preserveExisting; + return this; } /** diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequest.java b/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequest.java index c654d6926fa..fb4525a6842 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequest.java +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequest.java @@ -47,6 +47,7 @@ public class UpdateSettingsRequest extends AcknowledgedRequesttrue iff the settings update should only add but not update settings. If the setting already exists + * it should not be overwritten by this update. The default is false + */ + public boolean isPreserveExisting() { + return preserveExisting; + } + + /** + * Iff set to true this settings update will only add settings not already set on an index. Existing settings remain + * unchanged. + */ + public UpdateSettingsRequest setPreserveExisting(boolean preserveExisting) { + this.preserveExisting = preserveExisting; + return this; + } + /** * Sets the settings to be updated (either json/yaml/properties format) */ @@ -149,6 +167,7 @@ public class UpdateSettingsRequest extends AcknowledgedRequest(Priority.URGENT, request, listener) { @@ -221,7 +222,7 @@ public class MetaDataUpdateSettingsService extends AbstractComponent implements } int updatedNumberOfReplicas = openSettings.getAsInt(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, -1); - if (updatedNumberOfReplicas != -1) { + if (updatedNumberOfReplicas != -1 && preserveExisting == false) { routingTableBuilder.updateNumberOfReplicas(updatedNumberOfReplicas, actualIndices); metaDataBuilder.updateNumberOfReplicas(updatedNumberOfReplicas, actualIndices); logger.info("updating number_of_replicas to [{}] for indices {}", updatedNumberOfReplicas, actualIndices); @@ -239,6 +240,9 @@ public class MetaDataUpdateSettingsService extends AbstractComponent implements Settings.Builder updates = Settings.builder(); Settings.Builder indexSettings = Settings.builder().put(indexMetaData.getSettings()); if (indexScopedSettings.updateDynamicSettings(openSettings, indexSettings, updates, index.getName())) { + if (preserveExisting) { + indexSettings.put(indexMetaData.getSettings()); + } metaDataBuilder.put(IndexMetaData.builder(indexMetaData).settings(indexSettings)); } } @@ -250,6 +254,9 @@ public class MetaDataUpdateSettingsService extends AbstractComponent implements Settings.Builder updates = Settings.builder(); Settings.Builder indexSettings = Settings.builder().put(indexMetaData.getSettings()); if (indexScopedSettings.updateSettings(closedSettings, indexSettings, updates, index.getName())) { + if (preserveExisting) { + indexSettings.put(indexMetaData.getSettings()); + } metaDataBuilder.put(IndexMetaData.builder(indexMetaData).settings(indexSettings)); } } diff --git a/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index adffb8e9e01..c10774f8597 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -30,6 +30,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -221,9 +224,17 @@ public abstract class AbstractScopedSettings extends AbstractComponent { * * Validates that all given settings are registered and valid */ public final void validate(Settings settings) { - for (Map.Entry entry : settings.getAsMap().entrySet()) { - validate(entry.getKey(), settings); + List exceptions = new ArrayList<>(); + // we want them sorted for deterministic error messages + SortedMap sortedSettings = new TreeMap<>(settings.getAsMap()); + for (Map.Entry entry : sortedSettings.entrySet()) { + try { + validate(entry.getKey(), settings); + } catch (RuntimeException ex) { + exceptions.add(ex); + } } + ExceptionsHelper.rethrowAndSuppress(exceptions); } diff --git a/core/src/main/java/org/elasticsearch/common/settings/SettingsModule.java b/core/src/main/java/org/elasticsearch/common/settings/SettingsModule.java index 33233ff627e..2e7acd6ae8c 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/SettingsModule.java +++ b/core/src/main/java/org/elasticsearch/common/settings/SettingsModule.java @@ -20,13 +20,22 @@ package org.elasticsearch.common.settings; import org.elasticsearch.common.inject.AbstractModule; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.tribe.TribeService; +import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * A module that binds the provided settings to the {@link Settings} interface. @@ -37,9 +46,12 @@ public class SettingsModule extends AbstractModule { private final Set settingsFilterPattern = new HashSet<>(); private final Map> nodeSettings = new HashMap<>(); private final Map> indexSettings = new HashMap<>(); - private static final Predicate TRIBE_CLIENT_NODE_SETTINGS_PREDICATE = (s) -> s.startsWith("tribe.") && TribeService.TRIBE_SETTING_KEYS.contains(s) == false; + private static final Predicate TRIBE_CLIENT_NODE_SETTINGS_PREDICATE = (s) -> s.startsWith("tribe.") + && TribeService.TRIBE_SETTING_KEYS.contains(s) == false; + private final ESLogger logger; public SettingsModule(Settings settings) { + logger = Loggers.getLogger(getClass(), settings); this.settings = settings; for (Setting setting : ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) { registerSetting(setting); @@ -53,6 +65,56 @@ public class SettingsModule extends AbstractModule { protected void configure() { final IndexScopedSettings indexScopedSettings = new IndexScopedSettings(settings, new HashSet<>(this.indexSettings.values())); final ClusterSettings clusterSettings = new ClusterSettings(settings, new HashSet<>(this.nodeSettings.values())); + Settings indexSettings = settings.filter((s) -> s.startsWith("index.") && clusterSettings.get(s) == null); + if (indexSettings.isEmpty() == false) { + try { + String separator = IntStream.range(0, 85).mapToObj(s -> "*").collect(Collectors.joining("")).trim(); + StringBuilder builder = new StringBuilder(); + builder.append(System.lineSeparator()); + builder.append(separator); + builder.append(System.lineSeparator()); + builder.append("Found index level settings on node level configuration."); + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + int count = 0; + for (String word : ("Since elasticsearch 5.x index level settings can NOT be set on the nodes configuration like " + + "the elasticsearch.yaml, in system properties or command line arguments." + + "In order to upgrade all indices the settings must be updated via the /${index}/_settings API. " + + "Unless all settings are dynamic all indices must be closed in order to apply the upgrade" + + "Indices created in the future should use index templates to set default values." + ).split(" ")) { + if (count + word.length() > 85) { + builder.append(System.lineSeparator()); + count = 0; + } + count += word.length() + 1; + builder.append(word).append(" "); + } + + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + builder.append("Please ensure all required values are updated on all indices by executing: "); + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + builder.append("curl -XPUT 'http://localhost:9200/_all/_settings?preserve_existing=true' -d '"); + try (XContentBuilder xContentBuilder = XContentBuilder.builder(XContentType.JSON.xContent())) { + xContentBuilder.prettyPrint(); + xContentBuilder.startObject(); + indexSettings.toXContent(xContentBuilder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true"))); + xContentBuilder.endObject(); + builder.append(xContentBuilder.string()); + } + builder.append("'"); + builder.append(System.lineSeparator()); + builder.append(separator); + builder.append(System.lineSeparator()); + + logger.warn(builder.toString()); + throw new IllegalArgumentException("node settings must not contain any index level settings"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } // by now we are fully configured, lets check node level settings for unregistered index settings final Predicate acceptOnlyClusterSettings = TRIBE_CLIENT_NODE_SETTINGS_PREDICATE.negate(); clusterSettings.validate(settings.filter(acceptOnlyClusterSettings)); diff --git a/core/src/main/java/org/elasticsearch/rest/action/admin/indices/settings/RestUpdateSettingsAction.java b/core/src/main/java/org/elasticsearch/rest/action/admin/indices/settings/RestUpdateSettingsAction.java index bcf43a4baa6..4477ff25011 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/admin/indices/settings/RestUpdateSettingsAction.java +++ b/core/src/main/java/org/elasticsearch/rest/action/admin/indices/settings/RestUpdateSettingsAction.java @@ -47,6 +47,7 @@ public class RestUpdateSettingsAction extends BaseRestHandler { "timeout", "master_timeout", "index", + "preserve_existing", "expand_wildcards", "ignore_unavailable", "allow_no_indices")); @@ -62,6 +63,7 @@ public class RestUpdateSettingsAction extends BaseRestHandler { public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) { UpdateSettingsRequest updateSettingsRequest = updateSettingsRequest(Strings.splitStringByCommaToArray(request.param("index"))); updateSettingsRequest.timeout(request.paramAsTime("timeout", updateSettingsRequest.timeout())); + updateSettingsRequest.setPreserveExisting(request.paramAsBoolean("preserve_existing", updateSettingsRequest.isPreserveExisting())); updateSettingsRequest.masterNodeTimeout(request.paramAsTime("master_timeout", updateSettingsRequest.masterNodeTimeout())); updateSettingsRequest.indicesOptions(IndicesOptions.fromRequest(request, updateSettingsRequest.indicesOptions())); diff --git a/core/src/test/java/org/elasticsearch/common/settings/SettingsModuleTests.java b/core/src/test/java/org/elasticsearch/common/settings/SettingsModuleTests.java index e9694981539..7401164a4f6 100644 --- a/core/src/test/java/org/elasticsearch/common/settings/SettingsModuleTests.java +++ b/core/src/test/java/org/elasticsearch/common/settings/SettingsModuleTests.java @@ -36,12 +36,35 @@ public class SettingsModuleTests extends ModuleTestCase { { Settings settings = Settings.builder().put("cluster.routing.allocation.balance.shard", "[2.0]").build(); SettingsModule module = new SettingsModule(settings); - try { - assertInstanceBinding(module, Settings.class, (s) -> s == settings); - fail(); - } catch (IllegalArgumentException ex) { - assertEquals("Failed to parse value [[2.0]] for setting [cluster.routing.allocation.balance.shard]", ex.getMessage()); - } + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> assertInstanceBinding(module, Settings.class, (s) -> s == settings)); + assertEquals("Failed to parse value [[2.0]] for setting [cluster.routing.allocation.balance.shard]", ex.getMessage()); + } + + { + Settings settings = Settings.builder().put("cluster.routing.allocation.balance.shard", "[2.0]") + .put("some.foo.bar", 1).build(); + SettingsModule module = new SettingsModule(settings); + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> assertInstanceBinding(module, Settings.class, (s) -> s == settings)); + assertEquals("Failed to parse value [[2.0]] for setting [cluster.routing.allocation.balance.shard]", ex.getMessage()); + assertEquals(1, ex.getSuppressed().length); + assertEquals("unknown setting [some.foo.bar]", ex.getSuppressed()[0].getMessage()); + } + + { + Settings settings = Settings.builder().put("index.codec", "default") + .put("index.foo.bar", 1).build(); + SettingsModule module = new SettingsModule(settings); + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> assertInstanceBinding(module, Settings.class, (s) -> s == settings)); + assertEquals("node settings must not contain any index level settings", ex.getMessage()); + } + + { + Settings settings = Settings.builder().put("index.codec", "default").build(); + SettingsModule module = new SettingsModule(settings); + assertInstanceBinding(module, Settings.class, (s) -> s == settings); } } diff --git a/docs/reference/migration/migrate_5_0/settings.asciidoc b/docs/reference/migration/migrate_5_0/settings.asciidoc index 6dd15be0ed4..bf242a9929f 100644 --- a/docs/reference/migration/migrate_5_0/settings.asciidoc +++ b/docs/reference/migration/migrate_5_0/settings.asciidoc @@ -12,6 +12,15 @@ that plugins that define custom settings must register all of their settings during plugin loading using the `SettingsModule#registerSettings(Setting)` method. +==== Index Level Settings + +In previous versions Elasticsearch allowed to specify index level setting +as _defaults_ on the node level, inside the `elasticsearch.yaml` file or even via +command-line parameters. From Elasticsearch 5.0 on only selected settings like +for instance `index.codec` can be set on the node level. All other settings must be +set on each individual index. To set default values on every index, index templates +should be used instead. + ==== Node settings The `name` setting has been removed and is replaced by `node.name`. Usage of diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_settings.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_settings.json index 587fe027813..7c9cf627530 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_settings.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_settings.json @@ -16,6 +16,10 @@ "type": "time", "description": "Specify timeout for connection to master" }, + "preserve_existing": { + "type": "boolean", + "description": "Whether to update existing settings. If set to `true` existing settings on an index remain unchanged, the default is `false`" + }, "ignore_unavailable": { "type": "boolean", "description": "Whether specified concrete indices should be ignored when unavailable (missing or closed)" diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/10_basic.yaml index f9a453c90f1..cc9fb95b784 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/10_basic.yaml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/10_basic.yaml @@ -54,3 +54,70 @@ setup: body: number_of_replicas: 1 +--- +"Test preserve_existing settings": + - do: + indices.put_settings: + index: test-index + body: + number_of_replicas: 0 + + - do: + indices.get_settings: + flat_settings: false + + - match: + test-index.settings.index.number_of_replicas: "0" + + - do: + indices.put_settings: + preserve_existing: true + index: test-index + body: + index.number_of_replicas: 1 + index.translog.durability: "async" + + - do: + indices.get_settings: + flat_settings: false + + + - match: + test-index.settings.index.number_of_replicas: "0" + - match: + test-index.settings.index.translog.durability: "async" + + - do: + indices.close: + index: test-index + + - do: + indices.put_settings: + preserve_existing: true + index: test-index + body: + index.translog.durability: "request" + index.query_string.lenient: "true" + + - do: + indices.get_settings: + index: test-index + flat_settings: false + + - match: + test-index.settings.index.query_string.lenient: "true" + - match: + test-index.settings.index.translog.durability: "async" + + - do: + indices.open: + index: test-index + - do: + indices.get_settings: + index: test-index + flat_settings: false + + - match: + test-index.settings.index.query_string.lenient: "true" + - match: + test-index.settings.index.translog.durability: "async"