diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java index 248d86c7c4217..5aa64a5c1375e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java @@ -51,6 +51,8 @@ import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsResponse; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeResponse; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; import java.io.IOException; import java.util.Collections; @@ -456,4 +458,26 @@ public void putSettingsAsync(UpdateSettingsRequest updateSettingsRequest, Action UpdateSettingsResponse::fromXContent, listener, emptySet(), headers); } + /** + * Puts an index template using the Index Templates API + *

+ * See Index Templates API + * on elastic.co + */ + public PutIndexTemplateResponse putTemplate(PutIndexTemplateRequest putIndexTemplateRequest, Header... headers) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(putIndexTemplateRequest, RequestConverters::putTemplate, + PutIndexTemplateResponse::fromXContent, emptySet(), headers); + } + + /** + * Asynchronously puts an index template using the Index Templates API + *

+ * See Index Templates API + * on elastic.co + */ + public void putTemplateAsync(PutIndexTemplateRequest putIndexTemplateRequest, + ActionListener listener, Header... headers) { + restHighLevelClient.performRequestAsyncAndParseEntity(putIndexTemplateRequest, RequestConverters::putTemplate, + PutIndexTemplateResponse::fromXContent, listener, emptySet(), headers); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 705d8dfc9d252..720c934026b0b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -47,6 +47,7 @@ import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; @@ -77,7 +78,6 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.rankeval.RankEvalRequest; -import org.elasticsearch.rest.action.RestFieldCapabilitiesAction; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; @@ -86,10 +86,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.util.Collections; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import java.util.StringJoiner; final class RequestConverters { @@ -647,6 +644,21 @@ static Request indexPutSettings(UpdateSettingsRequest updateSettingsRequest) thr return request; } + static Request putTemplate(PutIndexTemplateRequest putIndexTemplateRequest) throws IOException { + String endpoint = new EndpointBuilder().addPathPartAsIs("_template").addPathPart(putIndexTemplateRequest.name()).build(); + Request request = new Request(HttpPut.METHOD_NAME, endpoint); + Params params = new Params(request); + params.withMasterTimeout(putIndexTemplateRequest.masterNodeTimeout()); + if (putIndexTemplateRequest.create()) { + params.putParam("create", Boolean.TRUE.toString()); + } + if (Strings.hasText(putIndexTemplateRequest.cause())) { + params.putParam("cause", putIndexTemplateRequest.cause()); + } + request.setEntity(createEntity(putIndexTemplateRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException { BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef(); return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType)); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java index eb09084200bd2..931447d85d44a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java @@ -56,11 +56,14 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeResponse; import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.broadcast.BroadcastResponse; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -73,11 +76,19 @@ import org.elasticsearch.rest.RestStatus; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; import java.util.Map; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; +import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractRawValues; +import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractValue; import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; @@ -812,4 +823,59 @@ public void testIndexPutSettingNonExistent() throws IOException { + "or check the breaking changes documentation for removed settings]")); } + @SuppressWarnings("unchecked") + public void testPutTemplate() throws Exception { + PutIndexTemplateRequest putTemplateRequest = new PutIndexTemplateRequest() + .name("my-template") + .patterns(Arrays.asList("pattern-1", "name-*")) + .order(10) + .create(randomBoolean()) + .settings(Settings.builder().put("number_of_shards", "3").put("number_of_replicas", "0")) + .mapping("doc", "host_name", "type=keyword", "description", "type=text") + .alias(new Alias("alias-1").indexRouting("abc")).alias(new Alias("{index}-write").searchRouting("xyz")); + + PutIndexTemplateResponse putTemplateResponse = execute(putTemplateRequest, + highLevelClient().indices()::putTemplate, highLevelClient().indices()::putTemplateAsync); + assertThat(putTemplateResponse.isAcknowledged(), equalTo(true)); + + Map templates = getAsMap("/_template/my-template"); + assertThat(templates.keySet(), hasSize(1)); + assertThat(extractValue("my-template.order", templates), equalTo(10)); + assertThat(extractRawValues("my-template.index_patterns", templates), contains("pattern-1", "name-*")); + assertThat(extractValue("my-template.settings.index.number_of_shards", templates), equalTo("3")); + assertThat(extractValue("my-template.settings.index.number_of_replicas", templates), equalTo("0")); + assertThat(extractValue("my-template.mappings.doc.properties.host_name.type", templates), equalTo("keyword")); + assertThat(extractValue("my-template.mappings.doc.properties.description.type", templates), equalTo("text")); + assertThat((Map) extractValue("my-template.aliases.alias-1", templates), hasEntry("index_routing", "abc")); + assertThat((Map) extractValue("my-template.aliases.{index}-write", templates), hasEntry("search_routing", "xyz")); + } + + public void testPutTemplateBadRequests() throws Exception { + RestHighLevelClient client = highLevelClient(); + + // Failed to validate because index patterns are missing + PutIndexTemplateRequest withoutPattern = new PutIndexTemplateRequest("t1"); + ValidationException withoutPatternError = expectThrows(ValidationException.class, + () -> execute(withoutPattern, client.indices()::putTemplate, client.indices()::putTemplateAsync)); + assertThat(withoutPatternError.validationErrors(), contains("index patterns are missing")); + + // Create-only specified but an template exists already + PutIndexTemplateRequest goodTemplate = new PutIndexTemplateRequest("t2").patterns(Arrays.asList("qa-*", "prod-*")); + assertTrue(execute(goodTemplate, client.indices()::putTemplate, client.indices()::putTemplateAsync).isAcknowledged()); + goodTemplate.create(true); + ElasticsearchException alreadyExistsError = expectThrows(ElasticsearchException.class, + () -> execute(goodTemplate, client.indices()::putTemplate, client.indices()::putTemplateAsync)); + assertThat(alreadyExistsError.getDetailedMessage(), + containsString("[type=illegal_argument_exception, reason=index_template [t2] already exists]")); + goodTemplate.create(false); + assertTrue(execute(goodTemplate, client.indices()::putTemplate, client.indices()::putTemplateAsync).isAcknowledged()); + + // Rejected due to unknown settings + PutIndexTemplateRequest unknownSettingTemplate = new PutIndexTemplateRequest("t3") + .patterns(Collections.singletonList("any")) + .settings(Settings.builder().put("this-setting-does-not-exist", 100)); + ElasticsearchStatusException unknownSettingError = expectThrows(ElasticsearchStatusException.class, + () -> execute(unknownSettingTemplate, client.indices()::putTemplate, client.indices()::putTemplateAsync)); + assertThat(unknownSettingError.getDetailedMessage(), containsString("unknown setting [index.this-setting-does-not-exist]")); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 1953c820b8af8..70c209c30abf2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -26,12 +26,11 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest; @@ -46,10 +45,11 @@ import org.elasticsearch.action.admin.indices.open.OpenIndexRequest; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.action.admin.indices.rollover.RolloverRequest; -import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.delete.DeleteRequest; @@ -70,6 +70,7 @@ import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.action.support.replication.ReplicationRequest; import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; @@ -77,15 +78,13 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.lucene.uid.Versions; -import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.client.RequestConverters.EndpointBuilder; -import org.elasticsearch.client.RequestConverters.Params; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.query.TermQueryBuilder; @@ -94,7 +93,6 @@ import org.elasticsearch.index.rankeval.RankEvalSpec; import org.elasticsearch.index.rankeval.RatedRequest; import org.elasticsearch.index.rankeval.RestRankEvalAction; -import org.elasticsearch.rest.action.RestFieldCapabilitiesAction; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; @@ -111,8 +109,6 @@ import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -121,7 +117,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.StringJoiner; import java.util.function.Consumer; import java.util.function.Function; @@ -1432,6 +1427,48 @@ public void testIndexPutSettings() throws IOException { assertEquals(expectedParams, request.getParameters()); } + public void testPutTemplateRequest() throws Exception { + Map names = new HashMap<>(); + names.put("log", "log"); + names.put("template#1", "template%231"); + names.put("-#template", "-%23template"); + names.put("foo^bar", "foo%5Ebar"); + + PutIndexTemplateRequest putTemplateRequest = new PutIndexTemplateRequest() + .name(randomFrom(names.keySet())) + .patterns(Arrays.asList(generateRandomStringArray(20, 100, false, false))); + if (randomBoolean()) { + putTemplateRequest.order(randomInt()); + } + if (randomBoolean()) { + putTemplateRequest.version(randomInt()); + } + if (randomBoolean()) { + putTemplateRequest.settings(Settings.builder().put("setting-" + randomInt(), randomTimeValue())); + } + if (randomBoolean()) { + putTemplateRequest.mapping("doc-" + randomInt(), "field-" + randomInt(), "type=" + randomFrom("text", "keyword")); + } + if (randomBoolean()) { + putTemplateRequest.alias(new Alias("alias-" + randomInt())); + } + Map expectedParams = new HashMap<>(); + if (randomBoolean()) { + expectedParams.put("create", Boolean.TRUE.toString()); + putTemplateRequest.create(true); + } + if (randomBoolean()) { + String cause = randomUnicodeOfCodepointLengthBetween(1, 50); + putTemplateRequest.cause(cause); + expectedParams.put("cause", cause); + } + setRandomMasterTimeout(putTemplateRequest, expectedParams); + Request request = RequestConverters.putTemplate(putTemplateRequest); + assertThat(request.getEndpoint(), equalTo("/_template/" + names.get(putTemplateRequest.name()))); + assertThat(request.getParameters(), equalTo(expectedParams)); + assertToXContentBody(putTemplateRequest, request.getEntity()); + } + private static void assertToXContentBody(ToXContent expectedBody, HttpEntity actualEntity) throws IOException { BytesReference expectedBytes = XContentHelper.toXContent(expectedBody, REQUEST_BODY_CONTENT_TYPE, false); assertEquals(XContentType.JSON.mediaTypeWithoutParameters(), actualEntity.getContentType().getValue()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java index 33cd12152851b..1dd9834d8f53f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java @@ -55,6 +55,10 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeResponse; import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateResponse; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.IndicesOptions; @@ -71,11 +75,14 @@ import org.elasticsearch.rest.RestStatus; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.hamcrest.Matchers.equalTo; + /** * This class is used to generate the Java Indices API documentation. * You need to wrap your code between two tags like: @@ -1598,4 +1605,164 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + public void testPutTemplate() throws Exception { + RestHighLevelClient client = highLevelClient(); + + // tag::put-template-request + PutIndexTemplateRequest request = new PutIndexTemplateRequest("my-template"); // <1> + request.patterns(Arrays.asList("pattern-1", "log-*")); // <2> + // end::put-template-request + + // tag::put-template-request-settings + request.settings(Settings.builder() // <1> + .put("index.number_of_shards", 3) + .put("index.number_of_replicas", 1) + ); + // end::put-template-request-settings + + { + // tag::put-template-request-mappings-json + request.mapping("tweet", // <1> + "{\n" + + " \"tweet\": {\n" + + " \"properties\": {\n" + + " \"message\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}", // <2> + XContentType.JSON); + // end::put-template-request-mappings-json + assertTrue(client.indices().putTemplate(request).isAcknowledged()); + } + { + //tag::put-template-request-mappings-map + Map jsonMap = new HashMap<>(); + Map message = new HashMap<>(); + message.put("type", "text"); + Map properties = new HashMap<>(); + properties.put("message", message); + Map tweet = new HashMap<>(); + tweet.put("properties", properties); + jsonMap.put("tweet", tweet); + request.mapping("tweet", jsonMap); // <1> + //end::put-template-request-mappings-map + assertTrue(client.indices().putTemplate(request).isAcknowledged()); + } + { + //tag::put-template-request-mappings-xcontent + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + { + builder.startObject("tweet"); + { + builder.startObject("properties"); + { + builder.startObject("message"); + { + builder.field("type", "text"); + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + request.mapping("tweet", builder); // <1> + //end::put-template-request-mappings-xcontent + assertTrue(client.indices().putTemplate(request).isAcknowledged()); + } + { + //tag::put-template-request-mappings-shortcut + request.mapping("tweet", "message", "type=text"); // <1> + //end::put-template-request-mappings-shortcut + assertTrue(client.indices().putTemplate(request).isAcknowledged()); + } + + // tag::put-template-request-aliases + request.alias(new Alias("twitter_alias").filter(QueryBuilders.termQuery("user", "kimchy"))); // <1> + request.alias(new Alias("{index}_alias").searchRouting("xyz")); // <2> + // end::put-template-request-aliases + + // tag::put-template-request-order + request.order(20); // <1> + // end::put-template-request-order + + // tag::put-template-request-version + request.version(4); // <1> + // end::put-template-request-version + + // tag::put-template-whole-source + request.source("{\n" + + " \"index_patterns\": [\n" + + " \"log-*\",\n" + + " \"pattern-1\"\n" + + " ],\n" + + " \"order\": 1,\n" + + " \"settings\": {\n" + + " \"number_of_shards\": 1\n" + + " },\n" + + " \"mappings\": {\n" + + " \"tweet\": {\n" + + " \"properties\": {\n" + + " \"message\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"aliases\": {\n" + + " \"alias-1\": {},\n" + + " \"{index}-alias\": {}\n" + + " }\n" + + "}", XContentType.JSON); // <1> + // end::put-template-whole-source + + // tag::put-template-request-create + request.create(true); // <1> + // end::put-template-request-create + + // tag::put-template-request-masterTimeout + request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1> + request.masterNodeTimeout("1m"); // <2> + // end::put-template-request-masterTimeout + + request.create(false); // make test happy + + // tag::put-template-execute + PutIndexTemplateResponse putTemplateResponse = client.indices().putTemplate(request); + // end::put-template-execute + + // tag::put-template-response + boolean acknowledged = putTemplateResponse.isAcknowledged(); // <1> + // end::put-template-response + assertTrue(acknowledged); + + // tag::put-template-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(PutIndexTemplateResponse putTemplateResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::put-template-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::put-template-execute-async + client.indices().putTemplateAsync(request, listener); // <1> + // end::put-template-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } } diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 1b5afe9d0b841..8dd8e64884533 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -107,7 +107,6 @@ ones that the user is authorized to access in case field level security is enabl Fixed prerelease version of elasticsearch in the `deb` package to sort before GA versions ({pull}29000[#29000]) -Respect accept header on requests with no handler ({pull}30383[#30383]) Rollup:: * Validate timezone in range queries to ensure they match the selected job when searching ({pull}30338[#30338]) @@ -118,6 +117,7 @@ Fail snapshot operations early when creating or deleting a snapshot on a reposit written to by an older Elasticsearch after writing to it with a newer Elasticsearch version. ({pull}30140[#30140]) Fix NPE when CumulativeSum agg encounters null value/empty bucket ({pull}29641[#29641]) +Do not fail snapshot when deleting a missing snapshotted file ({pull}30332[#30332]) //[float] //=== Regressions @@ -165,6 +165,8 @@ synchronous and predictable. Also the trigger engine thread is only started on data nodes. And the Execute Watch API can be triggered regardless is watcher is started or stopped. ({pull}30118[#30118]) +Added put index template API to the high level rest client ({pull}30400[#30400]) + [float] === Bug Fixes @@ -212,6 +214,8 @@ coming[6.3.1] Reduce the number of object allocations made by {security} when resolving the indices and aliases for a request ({pull}30180[#30180]) +Respect accept header on requests with no handler ({pull}30383[#30383]) + //[float] //=== Regressions diff --git a/docs/java-rest/high-level/indices/put_template.asciidoc b/docs/java-rest/high-level/indices/put_template.asciidoc new file mode 100644 index 0000000000000..7f0f3a1fee7f5 --- /dev/null +++ b/docs/java-rest/high-level/indices/put_template.asciidoc @@ -0,0 +1,168 @@ +[[java-rest-high-put-template]] +=== Put Template API + +[[java-rest-high-put-template-request]] +==== Put Index Template Request + +A `PutIndexTemplateRequest` specifies the `name` of a template and `patterns` +which controls whether the template should be applied to the new index. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request] +-------------------------------------------------- +<1> The name of the template +<2> The patterns of the template + +==== Settings +The settings of the template will be applied to the new index whose name matches the +template's patterns. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-settings] +-------------------------------------------------- +<1> Settings for this template + +[[java-rest-high-put-template-request-mappings]] +==== Mappings +The mapping of the template will be applied to the new index whose name matches the +template's patterns. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-mappings-json] +-------------------------------------------------- +<1> The type to define +<2> The mapping for this type, provided as a JSON string + +The mapping source can be provided in different ways in addition to the +`String` example shown above: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-mappings-map] +-------------------------------------------------- +<1> Mapping source provided as a `Map` which gets automatically converted +to JSON format + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-mappings-xcontent] +-------------------------------------------------- +<1> Mapping source provided as an `XContentBuilder` object, the Elasticsearch +built-in helpers to generate JSON content + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-mappings-shortcut] +-------------------------------------------------- +<1> Mapping source provided as `Object` key-pairs, which gets converted to +JSON format + +==== Aliases +The aliases of the template will define aliasing to the index whose name matches the +template's patterns. A placeholder `{index}` can be used in an alias of a template. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-aliases] +-------------------------------------------------- +<1> The alias to define +<2> The alias to define with placeholder + +==== Order +In case multiple templates match an index, the orders of matching templates determine +the sequence that settings, mappings, and alias of each matching template is applied. +Templates with lower orders are applied first, and higher orders override them. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-order] +-------------------------------------------------- +<1> The order of the template + +==== Version +A template can optionally specify a version number which can be used to simplify template +management by external systems. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-version] +-------------------------------------------------- +<1> The version number of the template + +==== Providing the whole source +The whole source including all of its sections (mappings, settings and aliases) +can also be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-whole-source] +-------------------------------------------------- +<1> The source provided as a JSON string. It can also be provided as a `Map` +or an `XContentBuilder`. + +==== Optional arguments +The following arguments can optionally be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-create] +-------------------------------------------------- +<1> To force to only create a new template; do not overwrite the existing template + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-request-masterTimeout] +-------------------------------------------------- +<1> Timeout to connect to the master node as a `TimeValue` +<2> Timeout to connect to the master node as a `String` + +[[java-rest-high-put-template-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-execute] +-------------------------------------------------- + +[[java-rest-high-put-template-async]] +==== Asynchronous Execution + +The asynchronous execution of a put template request requires both the `PutIndexTemplateRequest` +instance and an `ActionListener` instance to be passed to the asynchronous method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-execute-async] +-------------------------------------------------- +<1> The `PutIndexTemplateRequest` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `PutIndexTemplateResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument +<2> Called in case of failure. The raised exception is provided as an argument + +[[java-rest-high-put-template-response]] +==== Put Index Template Response + +The returned `PutIndexTemplateResponse` allows to retrieve information about the +executed operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[put-template-response] +-------------------------------------------------- +<1> Indicates whether all of the nodes have acknowledged the request diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 4d845e538415f..6623e09242f32 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -95,6 +95,7 @@ include::indices/update_aliases.asciidoc[] include::indices/exists_alias.asciidoc[] include::indices/put_settings.asciidoc[] include::indices/get_settings.asciidoc[] +include::indices/put_template.asciidoc[] == Cluster APIs diff --git a/docs/plugins/analysis-nori.asciidoc b/docs/plugins/analysis-nori.asciidoc index 9e6b8edcfa745..dd47ca819a7b2 100644 --- a/docs/plugins/analysis-nori.asciidoc +++ b/docs/plugins/analysis-nori.asciidoc @@ -278,7 +278,8 @@ It accepts the following setting: and defaults to: -``` +[source,js] +-------------------------------------------------- "stoptags": [ "E", "IC", @@ -288,7 +289,8 @@ and defaults to: "XPN", "XSA", "XSN", "XSV", "UNA", "NA", "VSV" ] -``` +-------------------------------------------------- +// NOTCONSOLE For example: diff --git a/docs/plugins/api.asciidoc b/docs/plugins/api.asciidoc index a2fbc5165ac94..4dffb9608821f 100644 --- a/docs/plugins/api.asciidoc +++ b/docs/plugins/api.asciidoc @@ -19,6 +19,9 @@ A number of plugins have been contributed by our community: * https://github.com/YannBrrd/elasticsearch-entity-resolution[Entity Resolution Plugin]: Uses http://github.com/larsga/Duke[Duke] for duplication detection (by Yann Barraud) + +* https://github.com/zentity-io/zentity[Entity Resolution Plugin] (https://zentity.io[zentity]): + Real-time entity resolution with pure Elasticsearch (by Dave Moore) * https://github.com/NLPchina/elasticsearch-sql/[SQL language Plugin]: Allows Elasticsearch to be queried with SQL (by nlpcn) diff --git a/docs/reference/indices/forcemerge.asciidoc b/docs/reference/indices/forcemerge.asciidoc index 26baf214176d1..fc56386a2773c 100644 --- a/docs/reference/indices/forcemerge.asciidoc +++ b/docs/reference/indices/forcemerge.asciidoc @@ -38,6 +38,12 @@ deletes. Defaults to `false`. Note that this won't override the `flush`:: Should a flush be performed after the forced merge. Defaults to `true`. +[source,js] +-------------------------------------------------- +POST /kimchy/_forcemerge?only_expunge_deletes=false&max_num_segments=100&flush=true + +-------------------------------------------------- + [float] [[forcemerge-multi-index]] === Multi Index diff --git a/docs/reference/search/request/preference.asciidoc b/docs/reference/search/request/preference.asciidoc index dbd9055ff8c86..4fd801c5f76e3 100644 --- a/docs/reference/search/request/preference.asciidoc +++ b/docs/reference/search/request/preference.asciidoc @@ -2,7 +2,8 @@ === Preference Controls a `preference` of which shard copies on which to execute the -search. By default, the operation is randomized among the available shard copies. +search. By default, the operation is randomized among the available shard +copies, unless allocation awareness is used. The `preference` is a query string parameter which can be set to: diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/11_reset.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/11_reset.yml index bc2dace0e1871..d7bd87cc73a82 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/11_reset.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_settings/11_reset.yml @@ -23,10 +23,15 @@ Test reset index settings: indices.get_settings: flat_settings: false - is_false: test-index.settings.index\.refresh_interval - - do: - indices.get_settings: - include_defaults: true - flat_settings: true - index: test-index - - match: - test-index.defaults.index\.refresh_interval: "1s" + +# Disabled until https://github.com/elastic/elasticsearch/pull/29229 is back-ported +# That PR changed the execution path of index settings default to be on the master +# until the PR is back-ported the old master will not return default settings. +# +# - do: +# indices.get_settings: +# include_defaults: true +# flat_settings: true +# index: test-index +# - match: +# test-index.defaults.index\.refresh_interval: "1s" diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java index 8cd1fac6f6fd1..b018e24a565b8 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java @@ -39,6 +39,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; @@ -58,14 +59,14 @@ import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; import static org.elasticsearch.common.settings.Settings.writeSettingsToStream; -import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; /** * A request to create an index template. */ -public class PutIndexTemplateRequest extends MasterNodeRequest implements IndicesRequest { +public class PutIndexTemplateRequest extends MasterNodeRequest implements IndicesRequest, ToXContent { private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(PutIndexTemplateRequest.class)); @@ -539,4 +540,34 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeOptionalVInt(version); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (customs.isEmpty() == false) { + throw new IllegalArgumentException("Custom data type is no longer supported in index template [" + customs + "]"); + } + builder.field("index_patterns", indexPatterns); + builder.field("order", order); + if (version != null) { + builder.field("version", version); + } + + builder.startObject("settings"); + settings.toXContent(builder, params); + builder.endObject(); + + builder.startObject("mappings"); + for (Map.Entry entry : mappings.entrySet()) { + Map mapping = XContentHelper.convertToMap(new BytesArray(entry.getValue()), false).v2(); + builder.field(entry.getKey(), mapping); + } + builder.endObject(); + + builder.startObject("aliases"); + for (Alias alias : aliases) { + alias.toXContent(builder, params); + } + builder.endObject(); + return builder; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateResponse.java index bf6e05a6c7b43..6c8a5291b12d5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateResponse.java @@ -21,6 +21,8 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; @@ -47,4 +49,14 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); writeAcknowledged(out); } + + private static final ConstructingObjectParser PARSER; + static { + PARSER = new ConstructingObjectParser<>("put_index_template", true, args -> new PutIndexTemplateResponse((boolean) args[0])); + declareAcknowledgedField(PARSER); + } + + public static PutIndexTemplateResponse fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java index 5be321734b5db..c90a7428268cd 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java @@ -184,7 +184,7 @@ protected ValidateQueryResponse newResponse(ValidateQueryRequest request, Atomic } @Override - protected ShardValidateQueryResponse shardOperation(ShardValidateQueryRequest request) throws IOException { + protected ShardValidateQueryResponse shardOperation(ShardValidateQueryRequest request, Task task) throws IOException { boolean valid; String explanation = null; String error = null; diff --git a/server/src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastAction.java b/server/src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastAction.java index 0961ab74c4703..60eaa19eaff63 100644 --- a/server/src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastAction.java @@ -84,11 +84,7 @@ protected final void doExecute(Request request, ActionListener listene protected abstract ShardResponse newShardResponse(); - protected abstract ShardResponse shardOperation(ShardRequest request) throws IOException; - - protected ShardResponse shardOperation(ShardRequest request, Task task) throws IOException { - return shardOperation(request); - } + protected abstract ShardResponse shardOperation(ShardRequest request, Task task) throws IOException; /** * Determines the shards this operation will be executed on. The operation is executed once per shard iterator, typically @@ -284,7 +280,7 @@ class ShardTransportHandler implements TransportRequestHandler { @Override public void messageReceived(ShardRequest request, TransportChannel channel, Task task) throws Exception { - channel.sendResponse(shardOperation(request)); + channel.sendResponse(shardOperation(request, task)); } @Override diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java index a04c75941e7b6..6b9992e7e4c3a 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java @@ -75,7 +75,8 @@ public interface BlobContainer { void writeBlob(String blobName, InputStream inputStream, long blobSize) throws IOException; /** - * Deletes a blob with giving name, if the blob exists. If the blob does not exist, this method throws an IOException. + * Deletes a blob with giving name, if the blob exists. If the blob does not exist, + * this method throws a NoSuchFileException. * * @param blobName * The name of the blob to delete. @@ -84,6 +85,21 @@ public interface BlobContainer { */ void deleteBlob(String blobName) throws IOException; + /** + * Deletes a blob with giving name, ignoring if the blob does not exist. + * + * @param blobName + * The name of the blob to delete. + * @throws IOException if the blob exists but could not be deleted. + */ + default void deleteBlobIgnoringIfNotExists(String blobName) throws IOException { + try { + deleteBlob(blobName); + } catch (final NoSuchFileException ignored) { + // This exception is ignored + } + } + /** * Lists all blobs in the container. * diff --git a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshots.java b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshots.java index d25b1eb04866d..34b07932e48ff 100644 --- a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshots.java +++ b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/BlobStoreIndexShardSnapshots.java @@ -102,13 +102,6 @@ private BlobStoreIndexShardSnapshots(Map files, List snapshots, int fileListGeneration, Map blobs) { - BlobStoreIndexShardSnapshots newSnapshots = new BlobStoreIndexShardSnapshots(snapshots); - // delete old index files first - for (String blobName : blobs.keySet()) { - if (indexShardSnapshotsFormat.isTempBlobName(blobName) || blobName.startsWith(SNAPSHOT_INDEX_PREFIX)) { - try { - blobContainer.deleteBlob(blobName); - } catch (IOException e) { - // We cannot delete index file - this is fatal, we cannot continue, otherwise we might end up - // with references to non-existing files - throw new IndexShardSnapshotFailedException(shardId, "error deleting index file [" - + blobName + "] during cleanup", e); - } + protected void finalize(final List snapshots, + final int fileListGeneration, + final Map blobs, + final String reason) { + final String indexGeneration = Integer.toString(fileListGeneration); + final String currentIndexGen = indexShardSnapshotsFormat.blobName(indexGeneration); + + final BlobStoreIndexShardSnapshots updatedSnapshots = new BlobStoreIndexShardSnapshots(snapshots); + try { + // If we deleted all snapshots, we don't need to create a new index file + if (snapshots.size() > 0) { + indexShardSnapshotsFormat.writeAtomic(updatedSnapshots, blobContainer, indexGeneration); } - } - // now go over all the blobs, and if they don't exist in a snapshot, delete them - for (String blobName : blobs.keySet()) { - // delete unused files - if (blobName.startsWith(DATA_BLOB_PREFIX)) { - if (newSnapshots.findNameFile(BlobStoreIndexShardSnapshot.FileInfo.canonicalName(blobName)) == null) { + // Delete old index files + for (final String blobName : blobs.keySet()) { + if (indexShardSnapshotsFormat.isTempBlobName(blobName) || blobName.startsWith(SNAPSHOT_INDEX_PREFIX)) { try { - blobContainer.deleteBlob(blobName); + blobContainer.deleteBlobIgnoringIfNotExists(blobName); } catch (IOException e) { - // TODO: don't catch and let the user handle it? - logger.debug(() -> new ParameterizedMessage("[{}] [{}] error deleting blob [{}] during cleanup", snapshotId, shardId, blobName), e); + logger.warn(() -> new ParameterizedMessage("[{}][{}] failed to delete index blob [{}] during finalization", + snapshotId, shardId, blobName), e); + throw e; } } } - } - // If we deleted all snapshots - we don't need to create the index file - if (snapshots.size() > 0) { - try { - indexShardSnapshotsFormat.writeAtomic(newSnapshots, blobContainer, Integer.toString(fileListGeneration)); - } catch (IOException e) { - throw new IndexShardSnapshotFailedException(shardId, "Failed to write file list", e); + // Delete all blobs that don't exist in a snapshot + for (final String blobName : blobs.keySet()) { + if (blobName.startsWith(DATA_BLOB_PREFIX) && (updatedSnapshots.findNameFile(canonicalName(blobName)) == null)) { + try { + blobContainer.deleteBlobIgnoringIfNotExists(blobName); + } catch (IOException e) { + logger.warn(() -> new ParameterizedMessage("[{}][{}] failed to delete data blob [{}] during finalization", + snapshotId, shardId, blobName), e); + } + } } + } catch (IOException e) { + String message = "Failed to finalize " + reason + " with shard index [" + currentIndexGen + "]"; + throw new IndexShardSnapshotFailedException(shardId, message, e); } } @@ -1003,7 +1007,7 @@ protected long findLatestFileNameGeneration(Map blobs) { if (!name.startsWith(DATA_BLOB_PREFIX)) { continue; } - name = BlobStoreIndexShardSnapshot.FileInfo.canonicalName(name); + name = canonicalName(name); try { long currentGen = Long.parseLong(name.substring(DATA_BLOB_PREFIX.length()), Character.MAX_RADIX); if (currentGen > generation) { @@ -1217,7 +1221,7 @@ public void snapshot(final IndexCommit snapshotIndexCommit) { newSnapshotsList.add(point); } // finalize the snapshot and rewrite the snapshot index with the next sequential snapshot index - finalize(newSnapshotsList, fileListGeneration + 1, blobs); + finalize(newSnapshotsList, fileListGeneration + 1, blobs, "snapshot creation [" + snapshotId + "]"); snapshotStatus.moveToDone(System.currentTimeMillis()); } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java b/server/src/test/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java index e48f151081f62..e4bb197f80ace 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/create/ShrinkIndexIT.java @@ -83,6 +83,7 @@ protected Collection> nodePlugins() { return Arrays.asList(InternalSettingsPlugin.class); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/30416") public void testCreateShrinkIndexToN() { int[][] possibleShardSplits = new int[][] {{8,4,2}, {9, 3, 1}, {4, 2, 1}, {15,5,1}}; int[] shardSplits = randomFrom(possibleShardSplits); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java index 72cbe2bd9ecab..294213452596f 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java @@ -20,10 +20,15 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.yaml.YamlXContent; @@ -35,6 +40,7 @@ import java.util.Collections; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; @@ -131,4 +137,52 @@ public void testValidateErrorMessage() throws Exception { assertThat(noError, is(nullValue())); } + private PutIndexTemplateRequest randomPutIndexTemplateRequest() throws IOException { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(); + request.name("test"); + if (randomBoolean()){ + request.version(randomInt()); + } + if (randomBoolean()){ + request.order(randomInt()); + } + request.patterns(Arrays.asList(generateRandomStringArray(20, 100, false, false))); + int numAlias = between(0, 5); + for (int i = 0; i < numAlias; i++) { + Alias alias = new Alias(randomRealisticUnicodeOfLengthBetween(1, 10)); + if (randomBoolean()) { + alias.indexRouting(randomRealisticUnicodeOfLengthBetween(1, 10)); + } + if (randomBoolean()) { + alias.searchRouting(randomRealisticUnicodeOfLengthBetween(1, 10)); + } + request.alias(alias); + } + if (randomBoolean()) { + request.mapping("doc", XContentFactory.jsonBuilder().startObject() + .startObject("doc").startObject("properties") + .startObject("field-" + randomInt()).field("type", randomFrom("keyword", "text")).endObject() + .endObject().endObject().endObject()); + } + if (randomBoolean()){ + request.settings(Settings.builder().put("setting1", randomLong()).put("setting2", randomTimeValue()).build()); + } + return request; + } + + public void testFromToXContentPutTemplateRequest() throws Exception { + for (int i = 0; i < 10; i++) { + PutIndexTemplateRequest expected = randomPutIndexTemplateRequest(); + XContentType xContentType = randomFrom(XContentType.values()); + BytesReference shuffled = toShuffledXContent(expected, xContentType, ToXContent.EMPTY_PARAMS, randomBoolean()); + PutIndexTemplateRequest parsed = new PutIndexTemplateRequest().source(shuffled, xContentType); + assertNotSame(expected, parsed); + assertThat(parsed.version(), equalTo(expected.version())); + assertThat(parsed.order(), equalTo(expected.order())); + assertThat(parsed.patterns(), equalTo(expected.patterns())); + assertThat(parsed.aliases(), equalTo(expected.aliases())); + assertThat(parsed.mappings(), equalTo(expected.mappings())); + assertThat(parsed.settings(), equalTo(expected.settings())); + } + } } diff --git a/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java b/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java index 2998ec8a6ba66..fab38a2b73b4a 100644 --- a/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java +++ b/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java @@ -22,7 +22,6 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.index.CorruptIndexException; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.NoShardAvailableActionException; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequestBuilder; @@ -61,10 +60,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import static org.elasticsearch.action.DocWriteResponse.Result.CREATED; +import static org.elasticsearch.action.DocWriteResponse.Result.UPDATED; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.not; /** @@ -135,7 +137,7 @@ public void testAckedIndexing() throws Exception { .setSource("{}", XContentType.JSON) .setTimeout(timeout) .get(timeout); - assertEquals(DocWriteResponse.Result.CREATED, response.getResult()); + assertThat(response.getResult(), isOneOf(CREATED, UPDATED)); ackedDocs.put(id, node); logger.trace("[{}] indexed id [{}] through node [{}], response [{}]", name, id, node, response); } catch (ElasticsearchException e) { diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java index 18be4e9437770..8aff12edc8a53 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java @@ -29,13 +29,14 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.NoSuchFileException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import static org.elasticsearch.repositories.ESBlobStoreTestCase.writeRandomBlob; import static org.elasticsearch.repositories.ESBlobStoreTestCase.randomBytes; import static org.elasticsearch.repositories.ESBlobStoreTestCase.readBlobFully; +import static org.elasticsearch.repositories.ESBlobStoreTestCase.writeRandomBlob; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; @@ -116,7 +117,7 @@ public void testDeleteBlob() throws IOException { try (BlobStore store = newBlobStore()) { final String blobName = "foobar"; final BlobContainer container = store.blobContainer(new BlobPath()); - expectThrows(IOException.class, () -> container.deleteBlob(blobName)); + expectThrows(NoSuchFileException.class, () -> container.deleteBlob(blobName)); byte[] data = randomBytes(randomIntBetween(10, scaledRandomIntBetween(1024, 1 << 16))); final BytesArray bytesArray = new BytesArray(data); @@ -124,7 +125,19 @@ public void testDeleteBlob() throws IOException { container.deleteBlob(blobName); // should not raise // blob deleted, so should raise again - expectThrows(IOException.class, () -> container.deleteBlob(blobName)); + expectThrows(NoSuchFileException.class, () -> container.deleteBlob(blobName)); + } + } + + public void testDeleteBlobIgnoringIfNotExists() throws IOException { + try (BlobStore store = newBlobStore()) { + BlobPath blobPath = new BlobPath(); + if (randomBoolean()) { + blobPath = blobPath.add(randomAlphaOfLengthBetween(1, 10)); + } + + final BlobContainer container = store.blobContainer(blobPath); + container.deleteBlobIgnoringIfNotExists("does_not_exist"); } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/tasks/MockTaskManager.java b/test/framework/src/main/java/org/elasticsearch/test/tasks/MockTaskManager.java index dec204537b917..41cdaefe03575 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/tasks/MockTaskManager.java +++ b/test/framework/src/main/java/org/elasticsearch/test/tasks/MockTaskManager.java @@ -57,7 +57,7 @@ public Task register(String type, String action, TaskAwareRequest request) { } catch (Exception e) { logger.warn( (Supplier) () -> new ParameterizedMessage( - "failed to notify task manager listener about unregistering the task with id {}", + "failed to notify task manager listener about registering the task with id {}", task.getId()), e); } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java index eef9e019b7a7e..620d575fc802c 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java @@ -110,8 +110,7 @@ public void clusterChanged(ClusterChangedEvent event) { // if this is not a data node, we need to start it ourselves possibly if (event.state().nodes().getLocalNode().isDataNode() == false && isWatcherStoppedManually == false && this.state.get() == WatcherState.STOPPED) { - watcherService.start(event.state()); - this.state.set(WatcherState.STARTED); + watcherService.start(event.state(), () -> this.state.set(WatcherState.STARTED)); return; } @@ -157,8 +156,8 @@ public void clusterChanged(ClusterChangedEvent event) { if (state.get() == WatcherState.STARTED) { watcherService.reload(event.state(), "new local watcher shard allocation ids"); } else if (state.get() == WatcherState.STOPPED) { - watcherService.start(event.state()); - this.state.set(WatcherState.STARTED); + this.state.set(WatcherState.STARTING); + watcherService.start(event.state(), () -> this.state.set(WatcherState.STARTED)); } } else { clearAllocationIds(); diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java index dcfb713a66580..49915674fe9e2 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java @@ -183,23 +183,40 @@ void reload(ClusterState state, String reason) { // by checking the cluster state version before and after loading the watches we can potentially just exit without applying the // changes processedClusterStateVersion.set(state.getVersion()); - pauseExecution(reason); triggerService.pauseExecution(); + int cancelledTaskCount = executionService.clearExecutionsAndQueue(); + logger.info("reloading watcher, reason [{}], cancelled [{}] queued tasks", reason, cancelledTaskCount); executor.execute(wrapWatcherService(() -> reloadInner(state, reason, false), e -> logger.error("error reloading watcher", e))); } - public void start(ClusterState state) { + /** + * start the watcher service, load watches in the background + * + * @param state the current cluster state + * @param postWatchesLoadedCallback the callback to be triggered, when watches where loaded successfully + */ + public void start(ClusterState state, Runnable postWatchesLoadedCallback) { + executionService.unPause(); processedClusterStateVersion.set(state.getVersion()); - executor.execute(wrapWatcherService(() -> reloadInner(state, "starting", true), + executor.execute(wrapWatcherService(() -> { + if (reloadInner(state, "starting", true)) { + postWatchesLoadedCallback.run(); + } + }, e -> logger.error("error starting watcher", e))); } /** - * reload the watches and start scheduling them + * reload watches and start scheduling them + * + * @param state the current cluster state + * @param reason the reason for reloading, will be logged + * @param loadTriggeredWatches should triggered watches be loaded in this run, not needed for reloading, only for starting + * @return true if no other loading of a newer cluster state happened in parallel, false otherwise */ - private synchronized void reloadInner(ClusterState state, String reason, boolean loadTriggeredWatches) { + private synchronized boolean reloadInner(ClusterState state, String reason, boolean loadTriggeredWatches) { // exit early if another thread has come in between if (processedClusterStateVersion.get() != state.getVersion()) { logger.debug("watch service has not been reloaded for state [{}], another reload for state [{}] in progress", @@ -221,9 +238,11 @@ private synchronized void reloadInner(ClusterState state, String reason, boolean executionService.executeTriggeredWatches(triggeredWatches); } logger.debug("watch service has been reloaded, reason [{}]", reason); + return true; } else { logger.debug("watch service has not been reloaded for state [{}], another reload for state [{}] in progress", state.getVersion(), processedClusterStateVersion.get()); + return false; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java index 6901adb0a6937..7b77afb225e4b 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java @@ -121,11 +121,25 @@ public void unPause() { } /** - * Pause the execution of the watcher executor + * Pause the execution of the watcher executor, and empty the state. + * Pausing means, that no new watch executions will be done unless this pausing is explicitely unset. + * This is important when watcher is stopped, so that scheduled watches do not accidentally get executed. + * This should not be used when we need to reload watcher based on some cluster state changes, then just calling + * {@link #clearExecutionsAndQueue()} is the way to go + * * @return the number of tasks that have been removed */ public int pause() { paused.set(true); + return clearExecutionsAndQueue(); + } + + /** + * Empty the currently queued tasks and wait for current executions to finish. + * + * @return the number of tasks that have been removed + */ + public int clearExecutionsAndQueue() { int cancelledTaskCount = executor.queue().drainTo(new ArrayList<>()); this.clearExecutions(); return cancelledTaskCount; diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java index 316cb722f2f1e..700901753d4a1 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java @@ -180,7 +180,7 @@ public void testManualStartStop() { reset(watcherService); when(watcherService.validate(clusterState)).thenReturn(true); lifeCycleService.clusterChanged(new ClusterChangedEvent("any", clusterState, stoppedClusterState)); - verify(watcherService, times(1)).start(eq(clusterState)); + verify(watcherService, times(1)).start(eq(clusterState), anyObject()); // no change, keep going reset(watcherService); @@ -423,7 +423,7 @@ public void testWatcherServiceDoesNotStartIfIndexTemplatesAreMissing() throws Ex when(watcherService.validate(eq(state))).thenReturn(true); lifeCycleService.clusterChanged(new ClusterChangedEvent("any", state, state)); - verify(watcherService, times(0)).start(any(ClusterState.class)); + verify(watcherService, times(0)).start(any(ClusterState.class), anyObject()); } public void testWatcherStopsWhenMasterNodeIsMissing() { diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherServiceTests.java index 5f815170215d3..73f9271e3efda 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherServiceTests.java @@ -68,6 +68,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -199,7 +200,7 @@ void stopExecutor() { when(client.clearScroll(any(ClearScrollRequest.class))).thenReturn(clearScrollFuture); clearScrollFuture.onResponse(new ClearScrollResponse(true, 1)); - service.start(clusterState); + service.start(clusterState, () -> {}); ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); verify(triggerService).start(captor.capture()); @@ -238,6 +239,27 @@ void stopExecutor() { verify(triggerEngine).pauseExecution(); } + // if we have to reload the watcher service, the execution service should not be paused, as this might + // result in missing executions + public void testReloadingWatcherDoesNotPauseExecutionService() { + ExecutionService executionService = mock(ExecutionService.class); + TriggerService triggerService = mock(TriggerService.class); + WatcherService service = new WatcherService(Settings.EMPTY, triggerService, mock(TriggeredWatchStore.class), + executionService, mock(WatchParser.class), mock(Client.class), executorService) { + @Override + void stopExecutor() { + } + }; + + ClusterState.Builder csBuilder = new ClusterState.Builder(new ClusterName("_name")); + csBuilder.metaData(MetaData.builder()); + + service.reload(csBuilder.build(), "whatever"); + verify(executionService).clearExecutionsAndQueue(); + verify(executionService, never()).pause(); + verify(triggerService).pauseExecution(); + } + private static DiscoveryNode newNode() { return new DiscoveryNode("node", ESTestCase.buildNewFakeTransportAddress(), Collections.emptyMap(), new HashSet<>(asList(DiscoveryNode.Role.values())), Version.CURRENT); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/integration/ExecutionVarsIntegrationTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/integration/ExecutionVarsIntegrationTests.java index 85b0280588a6e..2f69cc95a50ef 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/integration/ExecutionVarsIntegrationTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/integration/ExecutionVarsIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.function.Function; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.elasticsearch.xpack.watcher.actions.ActionBuilders.loggingAction; import static org.elasticsearch.xpack.watcher.client.WatchSourceBuilders.watchBuilder; import static org.elasticsearch.xpack.watcher.input.InputBuilders.simpleInput; @@ -36,6 +37,8 @@ public class ExecutionVarsIntegrationTests extends AbstractWatcherIntegrationTestCase { + private String watchId = randomAlphaOfLength(20); + @Override protected List> pluginTypes() { List> types = super.pluginTypes(); @@ -107,7 +110,7 @@ protected Map, Object>> pluginScripts() { public void testVars() throws Exception { WatcherClient watcherClient = watcherClient(); - PutWatchResponse putWatchResponse = watcherClient.preparePutWatch("_id").setSource(watchBuilder() + PutWatchResponse putWatchResponse = watcherClient.preparePutWatch(watchId).setSource(watchBuilder() .trigger(schedule(cron("0/1 * * * * ?"))) .input(simpleInput("value", 5)) .condition(new ScriptCondition( @@ -126,7 +129,7 @@ public void testVars() throws Exception { assertThat(putWatchResponse.isCreated(), is(true)); - timeWarp().trigger("_id"); + timeWarp().trigger(watchId); flush(); refresh(); @@ -135,11 +138,11 @@ public void testVars() throws Exception { // defaults to match all; }); - assertThat(searchResponse.getHits().getTotalHits(), is(1L)); + assertHitCount(searchResponse, 1L); Map source = searchResponse.getHits().getAt(0).getSourceAsMap(); - assertValue(source, "watch_id", is("_id")); + assertValue(source, "watch_id", is(watchId)); assertValue(source, "state", is("executed")); // we don't store the computed vars in history @@ -171,7 +174,7 @@ public void testVars() throws Exception { public void testVarsManual() throws Exception { WatcherClient watcherClient = watcherClient(); - PutWatchResponse putWatchResponse = watcherClient.preparePutWatch("_id").setSource(watchBuilder() + PutWatchResponse putWatchResponse = watcherClient.preparePutWatch(watchId).setSource(watchBuilder() .trigger(schedule(cron("0/1 * * * * ? 2020"))) .input(simpleInput("value", 5)) .condition(new ScriptCondition( @@ -193,13 +196,13 @@ public void testVarsManual() throws Exception { boolean debug = randomBoolean(); ExecuteWatchResponse executeWatchResponse = watcherClient - .prepareExecuteWatch("_id") + .prepareExecuteWatch(watchId) .setDebug(debug) .get(); assertThat(executeWatchResponse.getRecordId(), notNullValue()); XContentSource source = executeWatchResponse.getRecordSource(); - assertValue(source, "watch_id", is("_id")); + assertValue(source, "watch_id", is(watchId)); assertValue(source, "state", is("executed")); if (debug) {